第二篇:分治法之动态规划
目的:本篇博客并不具体的讨论某一个算法,而是将同类型的问题集中展示,从而对分治法有 更进一步的认识。
目录:
- 斐波那契数列问题
- 最长公共子序列
- 字符串相似度问题
- 最优二叉搜索树问题
- 0-1背包问题
问题1:斐波那契数列的问题
求解F(9),递归调用时,F(9)= F(8)+ F(7);
求解F(8),递归调用时,F(8)= F(7)+ F(6);
求解F(7),递归调用时,F(7)= F(6)+ F(5);
很明显,如此这般递归调用,会出现很多重复计算。当然熟悉的人都知道需要从底向上分别计算F(0),F(1),F(2)…..这样所有的节点都只是计算了一次,时间复杂度是O(n).
实施分治策略是基于一下几点认识:
1)小问题比大问题更容易解决
2)将小问题解答组装成大问题解答后所需要的成本比直接解决大问题的成本要低
3)小问题可以按照同样的方法分解成更小的问题
这个问题的提示是:分治法虽然将大的问题分解成了多个小的问题,带来了高效的解决方案,但是这种还是比较基础的分治,并没有对子问题的属性进行分析,从而丧失了对子问题属性加以利用的机会。
代码示例:
public static int Febonacci(int n){ int f1=1,f2 =1; int res=1; if(n<=0){ return 0; } if(n ==1 || n==2){ return res; } for(int i=3;i<=n;i++){ res = f1+f2; f1 = f2; f2 = res; } return res; }
在第一篇中只是实现了递归调用,所以说是使用了分治法。当对子问题进行了分析后,发现子问题有重叠,采取自底向上的办法,便是利用了子问题的性质,称之为动态规划。(当然,这个并不是动态规划的全部,动态规划更加重要的是优化,即不只是要解决一个问题,而且要以最优的方式解决。关于两个特性的关系后边会继续讨论)
问题2:最长公共子序列(Longest Common Subsequence)
最长公共子序列是指:设有两个序列S1[1,2 .. m]和S2[1,2…n],需要寻找它们之间的一个最长公共子序列。
例如:两个序列是
S1: G C C C T A G C G
S2: G C G C A A T G
S1和S2公共子序列有:GCG,GAG等,但是最长的是GCCAG。
解决办法:
设LCS(S1,S2)表示求解序列S1和S2最长公共子序列的长度的函数。
c[i,j] = LCS(S1[1,2…i],S2[1,2…j]); 则c[m,n]=LCS(S1,S2)(一个数值)
1)当S1[i] S2[j]相等时,c[i,j] = c[i-1,j-1];
2)当S1[i] S2[j]不等时,c[i,j] = max{c[i,j-1],c[i-1,j]}
证明:
当S1[i] S2[j]相等等时很容易证明,省略。
当S1[i] S2[j]不等时,假设S1[1,2…i]和S2[1,2…j]公共子序列S的最后一个字母是X。则有3中情况如下:
当S1[i] =X时,S 是S1[1,2…i]和S2[1,2…j-1]的公共子序列
当S2[j] =X时,S 是S1[1,2…i-1]和S2[1,2…j]的公共子序列
当S1[i] 和S2[j] 不等于X时,S 是S1[1,2…i-1]和S2[1,2…j-1]的公共子序列
综合以上:c[i,j] = max{c[i,j-1],c[i-1,j]}
伪代码:
If(S1[i] = S2[j]){
C[i,j]=c[i-1,j-1]+1;
}else{
C[i,j]=max{c[i-1,j],c[I,j-1]};
}
从上面可以看出,假设i=8,j=10,且S1[8] != S2[10], 求C[8,10]则需要求出
C[8,9]和C[7,10]。求C[8,9]可能需要求C[8,8]和C[7,9];C[7,10]可能需要求C[7,9]和C[6,10]。可以看出C[8,9]和C[7,10]是有公共部分的,同时需要求出C[7,9]。
上述的解决办法只是简单的运用了分治法,将一个大的问题拆分成两个更小规模的同类问题,自顶向下递归计算,还并没有使用到动态规划。而当分析了两个子问题的时候,发现两个子问题求解的时候是有重叠部分的,而显然重叠部分没必要算第二遍或者第三遍,这个时候可以采用像问题1斐波那契数列相同的办法自底向上求解,只是递归的落脚点和出口不太一样。
实例分析:
根据上面解决办法中可以看出,当i和j等于1的时候,如果S1[i] S2[j]不相等,则c[i,j] = max{c[i,j-1],c[i-1,j]},将c[i,0]和c[0,j]初始化为0
为了表示方便,使用了类似数组的方式,分别标上了下标如图。
第一行:
G 和 G相同,即S1[1]=S2[1],则C[1,1]=C[0,0]+1=0+1=1
G和C不同,即S1[1]!=S2[2],则C[1,2]=max{C[0,2],C[1,1]}=1
G和C不同, 即S1[1]!=S2[3],则C[1,3]=max{C[0,3],C[1,2]}= C[1,2]=1
G和C不同, 即S1[1]!=S2[4],则C[1,4]=max{C[0,4],C[1,3]}= C[1,3]=1
G和T不同, 即S1[1]!=S2[5],则C[1,5]=max{C[0,5],C[1,4]}= C[1,4]=1
G和A不同, 即S1[1]!=S2[6],则C[1,6]=max{C[0,6],C[1,5]}= C[1,5]=1
G和G相同, 即S1[1]=S2[7],则C[1,7]=C[0,6]+1=0+1=1
G和C不同, 即S1[1]!=S2[8],则C[1,8]=max{C[0,8],C[1,7]}= C[1,7]=1
G和G相同, 即S1[1]=S2[9],则C[1,9]=C[0,8]+1=0+1=1
以同样的方法完成第二行,结果如下
最终结果是:
可以看出C[8,9]=5.
找出最长公共子序列是:GCCAG
当然结果可能不止一个GCCTG同样也是最长公共子序列。
代码示例:
public static int[][] LCS(String s1,String s2){ int width = s1.length()+1; int length = s2.length()+1; int [][] res = new int[length][width]; //初始化 第0行第0列 for(int i =0;i<width;i++){ res[0][i] = 0; } for(int j =0;j<length;j++){ res[j][0] = 0; } for(int i = 1;i<length;i++){ char c1 = s2.charAt(i-1); for(int j =1;j<width;j++){ char c2 = s1.charAt(j-1); if(c1 == c2){ res[i][j] = res[i-1][j-1]+1; }else{ if(res[i-1][j]<=res[i][j-1]){ res[i][j]=res[i][j-1]; }else{ res[i][j]=res[i-1][j]; } } } } return res; }
问题3:字符串相似度问题
字符串相似度定义:把一个字符串(source)通过“插入、删除和替换”这样的编辑操作变成另外一个字符串(target)所需要的最少编辑次数,也就是两个字符串之间的编辑距离(edit distance)(转换的方法可能不唯一),转换的代价越高则说明两个字符串的相似度越低。比如两个字符串:“Halo”和“Hello”
下面给出两种将“Halo”转换成“Hello”的方法:
变换1:
Halo
Hello
Cost = 2 (插入e、替换a)
变换2:
Halo
Hello
Cost =3 (删除a、插入e,插入l)
解决办法:
该问题同样也可以使用分治法:
假如源字符串S[1,2,…m]和目标字符串T[1,2…n],函数Distance(S[1,2…m],T[1,2…n])表示求解字符串S转换成T消耗的最小代价。
设C[i.j] = Distance(S[1,2…m],T[1,2…n])
采取和问题3相同的办法:比较最后一个字符
如果S[i] = T[j],则C[i,j] = C[i-1,j-1];
如果S[i]!=T[j],则可以有三种选择方法,分别是插入、删除和替换。
采取插入时,代价是S[1,2…i]转化成T[1,2…j-1]代价+插入代价(1):C[i,j]=C[i,j-1]+1.
采取删除时,代价是S[1,2…i-1]转化成T[1,2…j]代价+删除代价(1):C[i,j]=C[i-1,j]+1.
采取替换时,代价是S[1,2…i-1]转化成T[1,2…j-1]代价+替换代价(1):C[i,j]=C[i-1,j-1]+1.
然后选择出最小代价min{ C[i,j-1]+1, C[i-1,j]+1, C[i-1,j-1]+1}.
到此处,我们运用了分治法把一个大的问题已经化成了1个或者3个同类规模更小的问题(至于这样计算,复杂度是变大还是变小,因问题而异。我们已知的是归并排序和快速排序就变得简单了)。
很显然,和问题3一样,该问题同样面临子问题有重叠的情况。这个时候就可以采用同类方法,使用动态规划策略,自底向上解决问题。
伪代码:
If(S[i] = T[j]){
C[i,j] = C[i-1,j-1];
}else{
C[i,j]=min{ C[i,j-1]+1, C[i-1,j]+1, C[i-1,j-1]+1}
}
使用递归,需要考虑出口问题,否则递归就像是空中楼阁一样,没有落脚点。和问题3一样,从上述代码中可以看出,当i,j=1的时候,需要考虑边界问题。同样在两个字符串前面添加一个空字符,其下标为0。
实例解析:
Source:Thirteen
Target: Thinking
初始化C[I,0]=I;C[0,j]=j,表示从空字符准换成其他字符需要消耗的代价。
初始化:
第一行:
T = T,即S[1] = T[1],C[1,1] = C[0,0]=0;
H!=T,即S[2]!=T[1],C[1,2] = min{C[0,1]+1,C[0,2]+1,C[1,1]+1}=min{1,2,2}=1
I!=T,即S[3]!=T[1], C[1,3] = min{C[0,2]+1,C[0,3]+1,C[1,2]+1}=min{3,4,2}=2
R!=T,即S[4]!=T[1], C[1,4] = min{C[0,3]+1,C[0,4]+1,C[1,3]+1}=min{4,5,3}=3
T=T,即S[5]=T[1], C[1,5] = min{C[0,4]+1,C[0,5]+1,C[1,4]+1}=min{5,6,4}=4
以此类推可以得每一行数据。
从上图可以看出最小代价是5. 时间、空间复杂度O(mn)
问题4:最优二叉搜索树问题
最优二叉搜索树是一棵搜索成本最低的二叉搜索树,它除了具有二叉搜索树需要具备的性质:
1)每个结点含有一个键值
2)每个结点最多有两个子孩子
3)每个结点的左孩子中的所有结点的值都小于该结点的值,右孩子中的所有结点的值都大于等于该结点的值。
除此性质之外,还需要具备搜索代价最小的性质。
例如有以下7个结点<K1,K2,K3,K4,K5,K6,K7>,K1<K2<K3<K4<K5<K6<K7,每个结点被搜索的概率为Pi.
对于普通的二叉搜索树,默认所有结点被搜索的概率是相同的。
即使如此我们还是可以构建不同的二叉搜索树。
结点搜索成本 = 结点深度*结点概率(假设root(K2或者K4)结点深度为1)
树1搜索成本:(1+2*2+3+4+5+6)*1/7 = 23/7
树2搜索成本:(1+2*2+3*4)*1/7 = 17/7
下面让问题更加具有一般化,即各个结点的概率并不都是相同的。
树1搜索成本:0.22+(0.08+0.22)*2+0.18*3+0.05*4+0.20*5+0.15*6=2.46
树2搜索成本:0.18+(0.22+0.20)*2+(0.12+0.08+0.05+0.15)*3=2.22
那么如何构建出搜索成本最低的二叉搜索树即最优二叉搜索树呢?这个很简单,分治法就可以。
假设已经确定了root结点是其中的一个,设为Kr. 想要保证整个树最优,必须保证左子树和右子树都是最优搜索树,否则我们就可以用最优的那棵树替换目前的。
所以我们用分治法,将一个大的问题就分解成了两个同样类型的小问题。
实例分析:
假设Kr = K3. 搜索成本用E(i,j)表示,W(i,j)表示所有i到j的概率相加求和。
则左子树组成部分是K1,K2,左子树的搜索成本是E(1,2)
则右子树组成部分是K4,K5,K6,K7,右子树的搜索成本是E(4,7)
则整个搜索树的成本是:
E(1,7)=E(1,2)+ E(4,7)+P1+(P2+P3)+(P4+P5+P6+P7)= E(1,2)+ E(4,7)+W(1,7)
P1+(P2+P3)+(P4+P5+P6+P7)这部分表示左子树或者右子树挂在了根结点K3上以后,右子树和左子树的每个子结点深度都增加1,右子树和左子树的搜索成本增加了对应的所有结点概率之和。
公式如下:
对于公式中j= i-1时,赋值为0,是为了处理边界问题,是一个技巧。如问题2和问题3中,在字符前面添加空字符,初始化下标为0等,
当然在含义上也能讲的通,即如果只有一个结点假设为K1,则E[1,1]=E[1,0]+E[2,1]+w(1,1)=0+0+P1=P1
从公式来看,显然子问题中存在了很多重复子问题,在分治之后,基于此种特性可以采用动态规划的方法,避免同一个子问题重复计算(这个特性并不是动态规划成功所必须,但是确实算法效率所必需的。)
但是上述结论存在一个未解决的问题:Kr应该选择谁?
选择Kr这一步,类似于问题2求最长公共子序列和问题3求字符串相似度问题中比较最后一个字符是否相等一样,先是通过比较该字符是否相等,进而将问题分解成了1个或者2个子问题。同样这里也是如此,只是需要每种可能都要计算即:Kr=K1,K2,K3,K4,K5,K6或者K7,7种情况,然后问题就被分解了。
实例分析:
初始化:需要初始化两个表分别是W和E表
W[i,i-1] = 0; 1<=i<=n
W[i,j] = W[i,j-1]+Pj, 1<=i<=j<=n
对于w[1,1], w[2,2], w[3,3]等和e[1,1],e[2,2],e[3,3]等也很容易理解,初始化后为:
对于w[i,j]很容易计算 :w[i,j]= w[i,j-1]+Pj;结果如下图
下面开始计算e[1,2]
e[1.2] = min{e[1,0]+e[2,2]+w[1,2] ,e[1,1]+e[3,2]+w[1,2]} =min{0.56,0.46} =0.46
e[2,3] = min{e[2,1]+e[3,3]+w[2,3] ,e[2,2]+e[4,3]+w[2,3]} =min{0.38,0.52} =0.38
e[3,4] = min{e[3,2]+e[4,4]+w[3,4] ,e[3,3]+e[5,4]+w[3,4]} =min{0.34,0.44} =0.34
……
最终的计算结果是
以上用红色和绿色是为了表明整个计算过程。很明显可以看出该过程并不是像问题1和问题2中如此的明显。
问题1中计算过程是线性的,计算了f(1),f(2),然后计算f(3)之后是f(4),很简单;
问题2中计算过程已经不是简单线性的,而是一行之后又一行,层次性的。
问题3中计算过程和问题2一样。
问题4中则是以一个斜线的方式向右上角推进,并且数量在逐次递减,红,绿,红,绿。
说这个过程实际上是为了说明用代码实现的时候,初始化是遵循这个逻辑。
代码实现:
public static void optimal_bst(float [] p){ int n = p.length; int length = n+2; int width = n+1; float w [][] = new float[length][width]; float e [][] = new float[length][width]; //初始化 for(int i=1;i<=n+1;i++){ w[i][i-1] = 0; e[i][i-1] = 0; } for(int l = 1;l <= n; l++){ for(int i=1;i<=n-l+1;i++){ int j=i+l-1; e[i][j] = Integer.MAX_VALUE; w[i][j] = w[i][j-1]+p[j-1]; float t; for(int r = i;r<=j;r++){ t = e[i][r-1]+e[r+1][j]+w[i][j]; if(t<e[i][j]){ e[i][j]=t; } } } }
这里有我的测试结果w表和e表
问题5:0-1背包问题
问题阐述:假设你因缘际会进入一个地宫,里面有各种奇珍异宝共n件,当你想尽可能多的带走珍宝的时候,无奈你只有一个背包,且背包能够承受的重量为Wkg,超过这个重量背包就会断裂不能够再使用。每件珍宝的重量w和价值p已知,那么你该如何选择珍宝才能是获得的价值最大?
很直接的办法是:计算出每件珍宝单位重量的价值,然后排序。依次拿去单位价值最大的珍宝。
此做法也许简单,但这个并不一定能保证你获得最大价值的珍宝。
举个例子。背包总承受能力50kg,有5件珍宝,价值和重量如下标
按照上述做法,选取1+2+4 总价值是190.
选取1+3+4 ,总价值是210。
选取2+3 ,总价值是220。
同样,对于这种最优问题我们可以试着去分析:像问题2最长公共子序列中,问题3字符串相似度比较某个字符是否相同和问题4最优二叉搜索树假设某个结点为根节点一样,先是用某种手段(分治手段),将整个大问题化为小规模的同类型问题。这种手段是对于某件珍宝采取要不放进去,要不就是不放进去。
如果不放进去:问题化为,在剩下4个珍宝中挑选放到50kg背包中。
如果放进去:问题化为,在剩下4个珍宝中挑选放到50kg-W(该珍宝重量)背包中。
于是我们顺利实施了分治法。同时这两个子问题都要求是最优的,即都是尽量带走更多价值的珍宝,和最初的问题一样只是规模变小。当然,这些子问题是有重叠的。
那么在符合这两个性质:最优子结构和重叠子问题的基础上,便可以顺利试试动态规划。
下一步就是数学建模问题,其实这个很关键,也很核心。这个问题前面所述并没有深入,也不好深入只是提醒自己和大家这个建模的能力很重要。
具体过程:
c[i][m]=max{c[i-1][m],c[i-1][m-w[i]]+p[i]}
这个就是问题转化成的数学公式。其中i表示珍宝的编号(对5件商品编号),m表示背包的重量。c[i-1][m]表示第i号珍宝放入背包,c[i-1][m-w[i]]+p[i]表示第i号珍宝不放入背包。然后两者取其大。
根据关系式:c[i][m]=max{c[i-1][m],c[i-1][m-w[i]]+p[i]}
此处需要特殊处理:当背包的重量m<w[i]的时候,c[i][m]=c[i-1][m]
这个比较容易理解,当背包的重量装不下该物品时,能装下的最大价值就是在相同重量m下,前i-1件中能装下的最大价值。
按道理这个算法已经结束,次数说一下自己考虑的时候遇到的问题,上述背包重量选择的时候,我选择了0,10,20,30,40,50.是因为5件物品的质量分别问10,20,30,10,20. 那么由于公式中有c[i][m-w[i]],所以用50分别减去这几个值就会得到10,20,30,40等4中情况,当m=40,30,20,10时,再一次减去各个物品质量会得到最终的结果0,10,20,30,40,50。如果自己走一边这个过程,就会发现或许本题有些取巧。对背包取值更加一般化可能需要以最小单位为1进行递增,像如下实例(各个珍宝重量不同与本题)
当然如此可以确保万无一失,但是用到本题中,会发现从0-50需要画多少方格。
虽然解题的过程可以取巧,但是计算机不能取巧,如果你用数组实现的话,下标就是一个问题,如果珍宝质量跨度非常大,这得需要多少内存。还没有想到很好的解决方案。这种解法并不具有一般性,没有做代码实现。但是整个过程还是很清晰的。
动态规划总结:
从上述5个案例中,其实就很容易理解前人总结出来的规律:想要实施动态规划需要具备两个性质:最优子结构和重叠子问题。
最优子结构是指解决大问题的时候,利用分治法分解出来的同类型小规模问题也是要达到最优才能确保大问题最优,这个性质是保证了不是只得到的一个结果这么低的要求,在有结果的基础上得到的是最优解。
重叠子问题是指分解得到的子问题虽然很多,但是有很多是重复的。这个是保证动态规划区别去普通递归的关键因素,是动态规划效率高的真正原因。
相关推荐
在IT领域,算法是解决问题的核心工具,而递归与分治策略、动态规划、贪心算法、回溯法、分支限界法以及随机化算法都是其中的重要组成部分。这些算法不仅适用于计算机科学,也在数据结构、优化问题和复杂计算中扮演着...
贪心算法、分治算法和动态规划是三种常用的算法设计策略,每种算法都有其特点和应用场景。下面我们将对这三种算法进行详细的比较和分析。 分治算法 分治算法是一种将原问题划分成若干个规模较小的子问题,然后递归...
下面我们将深入探讨解决这一问题的三种主要方法:蛮力法、分治法和动态规划法。 1. **蛮力法**: 蛮力法是最直观的解法,通过遍历所有可能的子序列来找出最大和。对于长度为n的数组,我们需要检查n*(n+1)/2个子...
此外,分治策略也为理解和应用其他高级算法,如动态规划和贪心算法奠定了基础。 “数据结构之分治策略.ppt”这份资源详细介绍了这些概念和应用,对于深入理解分治策略及其在数据结构中的应用具有极大的价值。通过...
贪心算法、动态规划和分治法的区别 贪心算法是指在当前看来是最好的结果,不考虑整体情况,只关心局部最优解。它从上往下,从顶部一步步最优,得到最后的结果,但不能保证全局最优解。贪心策略的选择也影响到结果。...
在IT领域,算法是解决问题的关键工具,而分治法、动态规划法和贪心算法是计算机科学中三种重要的算法设计策略。这些方法被广泛应用于各种实际问题,如数据排序、图论、最优化问题等。下面,我们将深入探讨这三个算法...
本压缩包文件包含的五个经典算法分别是分治策略、动态规划和回溯法,这些都是解决复杂问题的重要方法。 首先,分治策略是一种将大问题分解为小问题来解决的方法。它通常包含三个步骤:分解、解决和合并。例如,在C#...
通过记忆化或者动态规划的方法可以显著提高效率。 ##### 例3:阿克曼函数 阿克曼函数是一个双递归函数的例子,定义如下: \[ A(m,n) = \begin{cases} n+1 & \text{如果 } m = 0 \\ A(m-1, 1) & \text{如果 } m > ...
Kadane's Algorithm(卡丹算法)是一种使用分治策略解决最大子段和问题的高效方法。 1. 找出当前子数组的第一个元素arr[0],其最大子段和显然就是arr[0]。 2. 遍历剩余元素,对于每个元素arr[i],更新两个变量:...
本篇文章将深入探讨标题和描述中提到的一些核心算法,包括动态规划、分治算法、概率算法、模拟退火算法、搜索算法、贪婪算法、在线MATLAB应用、遗传算法以及组合算法。 1. **动态规划**:动态规划是一种解决具有...
本篇文章将深入探讨三种常用的算法:分治、递归、动态规划以及贪心策略,并通过实例来阐述它们的应用和重要性。 首先,让我们来看分治算法。分治法是一种处理大问题的策略,它将大问题分解为若干个规模较小、相互...
本文将深入探讨五大基础算法思想:分治、动态规划、贪心、回溯和分支界限,并与数据结构相结合,帮助我们理解和应用这些算法。 一、分治法 分治法是一种将复杂问题分解为较小、独立且相似的子问题的策略,然后对子...
3. **动态规划(Dynamic Programming)**: 动态规划通过将原问题分解为相互重叠的子问题,存储子问题的解,避免重复计算。经典的动态规划问题有斐波那契数列、最短路径问题等。动态规划强调最优子结构和无后效性。...
例如,如果子问题之间存在大量的重叠,那么分治法可能会导致不必要的计算,此时动态规划可能更为合适。此外,选择合适的子问题规模和数量对于优化算法性能至关重要。 在设计分治算法时,通常会设定一个阈值n0,当...
这个压缩包文件包含了四种基本的算法实现:回溯法、动态规划、分治法和贪心算法。这些都是计算机科学中极其重要的概念,对于理解和解决复杂问题至关重要。 1. **回溯法**:回溯法是一种试探性的解题策略,它尝试...
在IT领域,尤其是在算法设计与分析中,求解最大子段和问题是一个经典的问题,它不仅考验了程序员的基础算法知识,还涉及到了多种算法思想的应用,包括蛮力法、分治法以及动态规划法。下面将针对这三种方法进行详细的...
为了优化,可以采用动态规划或记忆化搜索来避免重复计算。 ### 4. 分治法基本思想 分治法包括三个步骤: 1. **分解(Divide)**:将大问题分解为若干个规模较小的相同问题。 2. **解决(Conquer)**:递归地解决...
总的来说,理解递归和掌握分治策略是C++编程中的重要技能,它们可以帮助程序员解决许多复杂的问题,例如排序、搜索、图论和动态规划等领域的算法设计。在编写递归函数时,确保正确设置边界条件和避免无限递归是至关...
- 动态规划与分治法、贪心法的区别。 - 动态规划的两种主要类型:最优化问题和计数问题。 - 动态规划的两大类别:一维动态规划和多维动态规划。 - 背包问题的动态规划解法,如完全背包、0-1背包和多重背包。 - 图论...
本系列实验报告涵盖了多个算法设计与分析的实验,包括动态规划、贪心算法、分治法和回溯法等算法思想的应用。实验任务多样,涉及矩阵链相乘问题、投资问题、背包问题、旅行商问题(TSP)、数字三角形、哈夫曼编码、...