分治法(3篇)之动态规划
? ? ? ? ? ? ? ? ? ? ? ? 第二篇:分治法之动态规划
目的:本篇博客并不具体的讨论某一个算法,而是将同类型的问题集中展示,从而对分治法有 ? ? ? 更进一步的认识。
目录:
斐波那契数列问题最长公共子序列字符串相似度问题最优二叉搜索树问题0-1背包问题 ?求解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
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; }
字符串相似度定义:把一个字符串(source)通过“插入、删除和替换”这样的编辑操作变成另外一个字符串(target)所需要的最少编辑次数,也就是两个字符串之间的编辑距离(edit distance)(转换的方法可能不唯一),转换的代价越高则说明两个字符串的相似度越低。比如两个字符串:“Halo”和“Hello”
下面给出两种将“Halo”转换成“Hello”的方法:
假如源字符串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[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一样,该问题同样面临子问题有重叠的情况。这个时候就可以采用同类方法,使用动态规划策略,自底向上解决问题。
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。
?第一行:
?T = T,即S[1] = T[1],C[1,1] = C[0,0]=0;
?从上图可以看出最小代价是5.? 时间、空间复杂度O(mn)
?即使如此我们还是可以构建不同的二叉搜索树。
?结点搜索成本 = 结点深度*结点概率(假设root(K2或者K4)结点深度为1)
?树1搜索成本:0.22+(0.08+0.22)*2+0.18*3+0.05*4+0.20*5+0.15*6=2.46
?对于公式中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[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]
?最终的计算结果是
?以上用红色和绿色是为了表明整个计算过程。很明显可以看出该过程并不是像问题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.
同样,对于这种最优问题我们可以试着去分析:像问题2最长公共子序列中,问题3字符串相似度比较某个字符是否相同和问题4最优二叉搜索树假设某个结点为根节点一样,先是用某种手段(分治手段),将整个大问题化为小规模的同类型问题。这种手段是对于某件珍宝采取要不放进去,要不就是不放进去。
于是我们顺利实施了分治法。同时这两个子问题都要求是最优的,即都是尽量带走更多价值的珍宝,和最初的问题一样只是规模变小。当然,这些子问题是有重叠的。
那么在符合这两个性质:最优子结构和重叠子问题的基础上,便可以顺利试试动态规划。
下一步就是数学建模问题,其实这个很关键,也很核心。这个问题前面所述并没有深入,也不好深入只是提醒自己和大家这个建模的能力很重要。
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个案例中,其实就很容易理解前人总结出来的规律:想要实施动态规划需要具备两个性质:最优子结构和重叠子问题。
最优子结构是指解决大问题的时候,利用分治法分解出来的同类型小规模问题也是要达到最优才能确保大问题最优,这个性质是保证了不是只得到的一个结果这么低的要求,在有结果的基础上得到的是最优解。