分治策略(3篇)之动态规划

                        第二篇:分治法之动态规划

目的本篇博客并不具体的讨论某一个算法,而是将同类型的问题集中展示,从而对分治法有       更进一步的认识

目录

  • 斐波那契数列问题
  • 最长公共子序列
  • 字符串相似度问题
  • 最优二叉搜索树问题
  • 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

S1S2公共子序列有:GCGGAG等,但是最长的是GCCAG

 

解决办法

LCS(S1,S2)表示求解序列S1S2最长公共子序列的长度的函数。

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] = maxc[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] = maxc[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=8j=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斐波那契数列相同的办法自底向上求解,只是递归的落脚点和出口不太一样。

实例分析

根据上面解决办法中可以看出,当ij等于1的时候,如果S1[i]  S2[j]不相等,则c[i,j] = maxc[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

GC不同,即S1[1]!=S2[2],C[1,2]=max{C[0,2],C[1,1]}=1

GC不同, S1[1]!=S2[3],C[1,3]=max{C[0,3],C[1,2]}= C[1,2]=1

GC不同, S1[1]!=S2[4],C[1,4]=max{C[0,4],C[1,3]}= C[1,3]=1

GT不同, S1[1]!=S2[5],C[1,5]=max{C[0,5],C[1,4]}= C[1,4]=1

GA不同, S1[1]!=S2[6],C[1,6]=max{C[0,6],C[1,5]}= C[1,5]=1

GG相同, S1[1]=S2[7],C[1,7]=C[0,6]+1=0+1=1

GC不同, S1[1]!=S2[8],C[1,8]=max{C[0,8],C[1,7]}= C[1,7]=1

GG相同, 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],函数DistanceS[1,2…m]T[1,2…n])表示求解字符串S转换成T消耗的最小代价。

C[i.j] = DistanceS[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一样,从上述代码中可以看出,当ij=1的时候,需要考虑边界问题。同样在两个字符串前面添加一个空字符,其下标为0

实例解析:

SourceThirteen

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.

 

对于普通的二叉搜索树,默认所有结点被搜索的概率是相同的。



 
即使如此我们还是可以构建不同的二叉搜索树。

 
结点搜索成本 = 结点深度*结点概率(假设rootK2或者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)表示所有ij的概率相加求和。

则左子树组成部分是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或者K77种情况,然后问题就被分解了。

 
实例分析:

初始化:需要初始化两个表分别是WE

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

 
问题50-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件中能装下的最大价值。

 
按道理这个算法已经结束,次数说一下自己考虑的时候遇到的问题,上述背包重量选择的时候,我选择了01020304050.是因为5件物品的质量分别问1020301020.  那么由于公式中有c[i][m-w[i]],所以用50分别减去这几个值就会得到102030404中情况,当m=40302010时,再一次减去各个物品质量会得到最终的结果01020304050。如果自己走一边这个过程,就会发现或许本题有些取巧。对背包取值更加一般化可能需要以最小单位为1进行递增,像如下实例(各个珍宝重量不同与本题)

 
当然如此可以确保万无一失,但是用到本题中,会发现从0-50需要画多少方格。

       虽然解题的过程可以取巧,但是计算机不能取巧,如果你用数组实现的话,下标就是一个问题,如果珍宝质量跨度非常大,这得需要多少内存。还没有想到很好的解决方案。这种解法并不具有一般性,没有做代码实现。但是整个过程还是很清晰的

 动态规划总结:

从上述5个案例中,其实就很容易理解前人总结出来的规律:想要实施动态规划需要具备两个性质:最优子结构和重叠子问题

最优子结构是指解决大问题的时候,利用分治法分解出来的同类型小规模问题也是要达到最优才能确保大问题最优,这个性质是保证了不是只得到的一个结果这么低的要求,在有结果的基础上得到的是最优解。

   重叠子问题是指分解得到的子问题虽然很多,但是有很多是重复的。这个是保证动态规划区别去普通递归的关键因素,是动态规划效率高的真正原因。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值