动态规划算法


本文为自己的总结记录,参考博文 动态规划以及 六大算法之三:动态规划总结很多。

算法思想及步骤

定义:

动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

基本认识:

已知问题规模为n的前提A,求解一个未知解B。(我们用An表示“问题规模为n的已知条件”)
此时,如果把问题规模降到0,即已知A0,可以得到A0->B.

  • 如果从A0添加一个元素,得到A1的变化过程。即A0->A1; 进而有A1->A2; A2->A3; …… ; Ai->Ai+1。这就是严格的归纳推理,也就是我们经常使用的数学归纳法;
  • 对于Ai+1,只需要它的上一个状态Ai即可完成整个推理过程(而不需要更前序的状态)。我们将这一模型称为马尔科夫模型。对应的推理过程叫做“贪心法”

然而,Ai与Ai+1往往不是互为充要条件,随着i的增加,有价值的前提信息越来越少,我们无法仅仅通过上一个状态得到下一个状态,因此可以采用如下方案:

  • {A1->A2}; {A1, A2->A3}; {A1,A2,A3->A4};……; {A1,A2,…,Ai}->Ai+1。这种方式就是第二数学归纳法。
  • 对于Ai+1需要前面的所有前序状态才能完成推理过程。我们将这一模型称为高阶马尔科夫模型。对应的推理过程叫做“动态规划法”。
    上述两种状态转移图如下图所示:
    在这里插入图片描述

思想:
能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
步骤
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

实现步骤:

1、创建一个一维数组或者二维数组,保存每一个子问题的结果,具体创建一维数组还是二维数组看题目而定,基本上如果题目中给出的是一个一维数组进行操作,就可以只创建一个一维数组,如果题目中给出了两个一维数组进行操作或者两种不同类型的变量值,比如背包问题中的不同物体的体积与总体积,找零钱问题中的不同面值零钱与总钱数,这样就需要创建一个二维数组。
注:需要创建二维数组的解法,都可以创建一个一维数组运用滚动数组的方式来解决,即一位数组中的值不停的变化。
2、设置数组边界值,一维数组就是设置第一个数字,二维数组就是设置第一行跟第一列的值,特别的滚动一维数组是要设置整个数组的值,然后根据后面不同的数据加进来变幻成不同的值。
3、找出状态转换方程,也就是说找到每个状态跟他上一个状态的关系,根据状态转化方程写出代码。
4、返回需要的值,一般是数组的最后一个或者二维数组的最右下角。

代码基本框架:
for(j=1; j<=m; j=j+1) // 第一个阶段  
  xn[j] = 初始值;  
for(i=n-1; i>=1; i=i-1)// 其他n-1个阶段  
  for(j=1; j>=f(i); j=j+1)//f(i)与i有关的表达式    
    xi[j]=j=max(或min{g(xi-[j1:j2]), ......, g(xi-1[jk:jk+1])};  
t = g(x1[j1:j2]); // 由子问题的最优解求解整个问题的最优解的方案  
print(x1[j1]);  
for(i=2; i<=n-1; i=i+1{       
  t = t-xi-1[ji];       
  for(j=1; j>=f(i); j=j+1)       
    if(t=xi[ji])            
      break;}

典型例子

数字三角形问题

在这里插入图片描述
在这里插入图片描述
首先要选最大和,即每一步都要选择一个最大值,满足最优化原理,而且不影响后续的选择,无后效性,对最终的结果有影响。符合动态规划的思想。
可以看出每走一步就是一个阶段,每一步的最优解就是选择改行的最大值。
由于最后一行可以确定,可以当做边界条件,所以我们自然而然想到递归求解。
在这里插入图片描述
大概思路:
在这里插入图片描述
但是这个方法存在大量的重复运算,时间复杂度为2的n次方。如果我们每次都把结果保存下来,复杂度就会大大降低。
在这里插入图片描述
在这里插入图片描述

斐波那契数列

原先的递归

if(n == 0){
   return 0;   
}else if(n == 1){	
   return 1;    
}else{	
    return solutionFibonacci(n-1)+solutionFibonacci(n-2);    }

使用动态规划

if(n == 0){
   return 0;   
}else if(n == 1){	
   return 1;    
}else{	
    int result[] = new int[n+1];
    result[0] = 0;
    result[1] = 1;
    for(int i=2;i<n;i++)
    {
      result[i] = result[i-1] + result[i-2];
    }
    return result[n];     
}

数组最大不连续递增子序列

arr[] = {3,1,4,1,5,9,2,6,5}的最长递增子序列长度为4。即为:1,4,5,9

设置一个数组temp,长度为原数组长度,数组第i个位置上的数字代表0…i上最长递增子序列,当增加一个数字时,最大递增子序列可能变成前面最大的递增子序列+1,也可能就是前面的最大递增子序列,这需要让新增加进来的数字arr[i]跟前面所有数字比较大小,即当 arr[i] > arr[j],temp[i] = max{temp[j]}+1,其中,j 的取值范围为:0,1…i-1,当 arr[i] < arr[j],temp[i] = max{temp[j]},j 的取值范围为:0,1…i-1,所以在状态转换方程为temp[i]=max{temp[i-1], temp[i-1]+1}。
就是在每个位置的最长递增子序列,要么是之前的+1,要么直接用之前的数,比如arr数组,第一个为1,第二个比之前的小直接继承前面的数1,第三个比第一个大加1,比第二个大加1,取最大的数2所以第三个数为2,第四个数比第一个第二个第三个都小,都继承1最后取最大值1,以此类推。

public static int MaxChildArrayOrder(int a[]) {	
	int n = a.length;		
	int temp[] = new int[n];//temp[i]代表0...i上最长递增子序列		
	for(int i=0;i<n;i++){	
		temp[i] = 1;//初始值都为1		
	}		
	for(int i=1;i<n;i++){		
		for(int j=0;j<i;j++){
			if(a[i]>a[j]&&temp[j]+1>temp[i])
			{//如果有a[i]比它前面所有的数都大,
			//则temp[i]为它前面的比它小的数的那一个temp+1取得的最大值 
			 temp[i]=temp[j]+1;				}			}		}		
			 int max = temp[0];//从temp数组里取出最大的值		
			 for(int i=1;i<n;i++){			
			 if(temp[i]>max){			
			 	max =temp[i];		
			}		
		}		
		return max;	
	}

每一步都要跟之前的数比较来确定要+1还是直接继承,满足最优原理且无无后效性,而且对后面值的大小提供了有效的信息,满足动态规划的思想。

数组最大连续子序列和

如arr[] = {6,-1,3,-4,-6,9,2,-2,5}的最大连续子序列和为14。即为:9,2,-2,5
创建一个数组a,长度为原数组长度,不同位置数字a[i]代表0…i上最大连续子序列和,a[0]=arr[0]设置一个最大值max,初始值为数组中的第一个数字。当进来一个新的数字arr[i+1]时,判断到他前面数字子序列和a[i]+arr[i+1]跟arr[i+1]哪个大,前者大就保留前者,后者大就说明前面连续数字加起来都不如后者一个新进来的数字大,前面数字就可以舍弃,从arr[i+1]开始,每次比较完都跟max比较一下,最后的max就是最大值。
就是之前分治算法中的滑动窗口。
每一步都要比较sum+a[i]和 a[i]然后取较大值,满足最优原理且无后效性,然后再通过比较更新max,为以后的取值提供有效的信息,满足动态规划的思想。

public static int MaxContinueArraySum(int a[]) {
 int n = a.length;
 int max = a[0];
 int sum = a[0];
 for(int i=1;i<n;i++){
  sum = Math.max(sum+a[i], a[i]);
  if(sum>=max){
     max = sum;
  }
 }
 return max;
}

两个字符串最大公共子序列

比如字符串1:BDCABA;字符串2:ABCBDAB,则这两个字符串的最长公共子序列长度为4,最长公共子序列是:BCBA
具体思想:设 X=(x1,x2,…xn)和 Y={y1,y2,…ym} 是两个序列,将 X 和 Y 的最长公共子序列记为LCS(X,Y),如果 xn=ym,即X的最后一个元素与Y的最后一个元素相同,这说明该元素一定位于公共子序列中。因此,现在只需要找:LCS(Xn-1,Ym-1)就好,LCS(X,Y)=LCS(Xn-1,Ym-1)+1;如果xn != ym,这下要麻烦一点,因为它产生了两个子问题:LCS(Xn-1,Ym) 和 LCS(Xn,Ym-1)。
动态规划解法:先创建一个解空间即数组,因为给定的是两个字符串即两个一维数组存储的数据,所以要创建一个二维数组,设字符串X有n个值,字符串Y有m个值,需要创建一个m+1*n+1的二维数组,二维数组每个位置(i,j)代表当长度为i的X子串与长度为j的Y的子串他们的最长公共子串,之所以要多创建一个是为了将边界值填入进去,边界值就是第一行跟第一列,指X长度为0或者Y长度为0时,自然需要填0,其他位置填数字时,当这两个位置数字相同,dp[i][j] = dp[i-1][j-1]+1;当这两个位置数字不相同时,dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j])。最后二维数组最右下角的值就是最大子串。
每一个字符都要进行比较,然后确定该位置值时需要比较是否相同,相同时直接加一,不相同时则需要比较LCS(Xn-1,Ym) 和 LCS(Xn,Ym-1),这是两个子问题,满足最优原理,无后效性。符合动态规划的思想。

public class MaxTwoArraySameOrder {
 public static int MaxTwoArraySameOrderMethod(String str1,String str2) {
  int m = str1.length();
  int n = str2.length();
  /*
   * 定义一个二维数组保存公共子序列长度
   * dp[i][j]表示字符串1从头开始长度是i,字符串2从头开始长度是j,这两个字符串的最长公共子序列的长度
   * 设置数组行列比他们长度大一往二维数组中填写数字时,每个位置的数字跟他上方或者左方或者左上方数字有关系,这样处理边界数字时不用处理这种情况,方便接下来的循环
   */
  int dp[][] = new int[m+1][n+1];
  /*
   * 初始化第一行第一列
   * dp[0,j]表示啥?表示字符串1的长度是0,字符串2的长度是j,这两个字符串的最长公共子序列的长度是0,因为,字符串1 根本就没有嘛
   */
  for(int i=0;i<=m;i++){
   dp[i][0] = 0;
  }
  for(int i=0;i<=n;i++){
   dp[0][i] = 0;
  }
  for(int i=1;i<=m;i++){
   for(int j=1;j<=n;j++){
    /*
     * 如果当c[i][j]时,字符串1从头开始长度是i,字符串2从头开始长度是j时他们最后一个字符相同
     * 就同时把他们向前移动一位,找c[i-1][j-1]时长度最大的再加一
     * 表现在二维数组中就是c[i][j]左上方的点
     */
    if(str1.charAt(i-1) == str2.charAt(j-1)){
     dp[i][j] = dp[i-1][j-1]+1;
     /*
      * 如果当c[i][j]时,他们最后一个字符不相同
      * 要将str1往前移动一位的c[i-1][j]的lcs长度,或者将str2往前移动一位的c[i][j-1]的lcs长度
      * 哪个长,将它赋给c[i][j]
      * 表现在二维数组中就是c[i][j]上方的点或者左方的点
      */
    }else{
     dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]);
    }
   }
  }
  return dp[m][n];
 }
 public static void main(String[] args) {
  String str1 = "BDCABA";
  String str2 = "ABCBDAB";
  int array = MaxTwoArraySameOrderMethod(str1,str2);
  System.out.println(array);
 }
}

背包问题

在N件物品取出若干件放在容量为W的背包里,每件物品的体积为W1,W2……Wn(Wi为整数),与之相对应的价值为P1,P2……Pn(Pi为整数),求背包能够容纳的最大价值。

像这种固定数值的组合问题,比如这个问题的W总容量,跟下个实例零钱问题的总钱数,都是适合用动态规划来解决的问题,对于这样的问题,动态规划的解法就是:创建一个二维数组,横坐标是从1开始到W,纵坐标是组成W的各种元素,本题中就是指W1,W2……Wn,数组中每个位置(i,j)的数字就是当组成元素只有W1,W2……Wi,背包可放容量为j时的结果,本题中就是容纳的最大价值。所以很容易分析出,当(i,j)时,如果Wi能放的下,空间减小,但是会增加Pi的价值,如果Wi不能放的下,空间不变,是(i-1,j)的价值,取其中最大值就好了,即状态转化方程为能放的下,dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-w[i]]+p[i]);放不下,dp[i][j] = dp[i-1][j];

首先是分解原问题为子问题,将每加入一个物品视为每一步,当前有两个变量,一个是体积j的变化,一个放入物品i后背包内物品总价值的变化。子问题控制一个变量,即当前体积确定的时候,背包容量为j时,前i个物品所能达到的最大价值。所以要创建一个二维数组。
其次是确定状态,就是几个变量之间的关系,就是我们上面说的背包容量为j时,前i个物品所能达到的最大价值。
确定一些边界条件,比如初始值和结束值。初始值是dp[0][j]=0,背包里没有物品时,价值为0,结束值就是我们二维数组的最后一个值。
确定状态转移方程,第i个物品体积为w,价值为p,则状态转移方程为

j<w dp[i][j] = dp[i-1][j]//放不进去就是上一个物品的最大价值
j>w dp[i][j] = max{dp[i-1][j],dp[i-1][j-list[i].w]+v}//后面那个的意思是容量j减去一个和i相同的体积,再加上i的价值,就是相当于放入了一个i

public static int PackageHelper(int n,int w[],int p[],int v) {
  //设置一个二维数组,横坐标代表从第一个物品开始放到第几个物品,纵坐标代表背包还有多少容量,dp代表最大价值
  int dp[][] = new int[n+1][v+1];
  for(int i=1;i<n+1;i++){
   for(int j=1;j<=v;j++){
    if(j>=w[i]){
     /*
      * 当能放得下这个物品时,放下这个物品,价值增加,但是空间减小,最大价值是dp[i-1][j-w[i]]+p[i]
      * 当不放这个物品时,空间大,物品还是到i-1,最大价值是dp[i-1][j]
      * 比较这两个大小,取最大的,就是dp[i][j]
      */
     dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-w[i]]+p[i]);
    }else{
     //如果放不下,就是放上一个物品时的dp
     dp[i][j] = dp[i-1][j];
    }
   }
  }
  return dp[n][v];
 }

动态规划里很多二维数组都可以用滚动数组来优化,滚动数组是一维的。长度为从1到W,初始值都是0,能装得下i时,dp[j] = Math.max(dp[j], dp[j-w[i]]+p[i]);装不下时,dp[j] = dp[j];

public static int PackageHelper2(int n,int w[],int p[],int v) {
  //设置一个二维数组,横坐标代表从第一个物品开始放到第几个物品,纵坐标代表背包还有多少容量,dp代表最大价值
  int dp[] = new int[v+1];
  for(int i=1;i<=n;i++){
   for(int j=v;j>0;j--){
    if(j>w[i]){
     dp[j] = Math.max(dp[j], dp[j-w[i]]+p[i]);
    }else{
     dp[j] = dp[j];
    }
   }
  }
  return dp[v];
 }

找零钱问题:有几种方法

具体思路同背包问题,这里只分析一下动态转化方程,能用这种零钱,分为用了这种零钱的方法跟没用到这种零钱的方法,dp[i][j] = dp[i-1][j] + dp[i][j-num[i]];如果不能用这种零钱,即所组成的面额小于当前零钱,直接等于不用这种零钱的数值,dp[i][j] = dp[i-1][j]。这里要特别注意的是。1、开始填写二维数组边界值时,第一行是填写只用第一种面额零钱组成相应数额的方法,要注意是总数额除以第一种面额取余为0才能组成,即如果第一种面额为2,不能组成3,5的数额等;2、填写二维数组第一列时,代表到用到面额为i时,剩余数额为0,即只用i就可以组成相应数额,这也是一种方法,所以第一列的值,第一个为0,后面全为1。

public static int SmallMoney(int num[],int target) {
  int m = num.length;
  int dp[][] = new int[m][target+1];
  dp[0][0] = 1;
  for(int i=1;i<=target;i++){
   if(i%num[0] == 0){
    dp[0][i] = 1;//第一行数值填写
   }else{
    dp[0][i] = 0;
   }
  }
  for(int i=0;i<m;i++){
   dp[i][0] = 1;//第一列数值填写
  }
  for(int i=1;i<m;i++){
   for(int j=1;j<=target;j++){
    if(j<num[i]){
     dp[i][j] = dp[i-1][j];
    }else{
     dp[i][j] = dp[i-1][j] + dp[i][j-num[i]];
    }
   }
  }
  return dp[m-1][target];
 }

总结

解决动态规划的问题
首先要确定这个问题是否适用于动态规划,看他是否能分为好多步,每一步是否能够求得当前的最优解,是否受后面的影响,是否能对后面的结果提供有效信息。即是否满足最优定理,是否无后效性,是否能有重叠子的问题。
拆分原问题,划分为子问题,一定要找出其中的变量,其中的一个变量在另一个变量确定的情况下跟原问题有关,比如背包问题中,原问题是求背包装最大价值的东西,子问题中一个变量就是,容量确定的情况下,前i个物品所能达到的最大价值。
确定状态,什么是状态,状态如何描述,比如背包问题,状态就可以描述为,背包容量为j时,前i个物品所能达到的最大价值。就是要找到几个变量之间的关系,最好将一个变量固定,来找和另一个变量之间的关系。
寻找边界条件,比如初始值和结束值,一个变量为0时,他的值是多少,最终的结果存放在什么地方,比如数组一般是存放在数组最后一位。
确定状态转移方程,首先是分情况,先写出简单的情况,然后复杂情况大概率是一个递归式,把这个递归式写出来。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值