看了知乎用户徐凯强 Andy与王勐回答动态规划提问感觉受益匪浅。在这写下自己理解。
首先要明确,动态规划的本质是找到合适的状态的定义与找到状态转移方程。递归与递推只是实现动态规划的工具。动态规划是思想,递推与递归只是方法。不能本末倒置。
什么是状态与状态转移方程两个人的文章已经进行了详细的说明。简单来说,对于一个问题,如果你想使用动态规划来解决那么必须找到DP方程。而且你定义这个DP方程必须无后效性。找到DP方程后,通过DP方程就可以使用递归或者递推方法解决。
找DP方程是解决动态规划问题的核心。
王勐的总结很好
每个阶段的最优状态都是由上一个阶段的最优状态得到的->贪心;
每个阶段的最优状态是由之前所有阶段的状态的组合得到的->搜索;
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而不管之前这个状态是如何得到的->动态规划。
0-1 背包问题:给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi 。
问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
如果背包物品可以部分放入,那么这个问题变成了贪心问题。动态规划的一种特殊情况。每次只讲性价比较高的物品放入即可。
对于0-1背包问题这里有两种思路:
1.暴力枚举:尝试将每件物品放入背包中,直到背包容量不足。计算当前组合的价值。最后找到所有组合的最大值。这里用到的便是搜索,当前阶段需要之前所有阶段的状态组合。
2.动态规划:当前背包状态下的最大价值等于,MAX(i-1个包容量为j时计算出最大价值,v(i) +(i-1个包容量为j-w(i)时计算的最大价值))。即求两者的最大值,求当前问题用到了子问题。且两个问题解决方法相同。
暴力枚举代码:
1 /*测试结果10 2 3 6 3 1 3 5 4 4 3 6 5 */ 6 #include<stdio.h> 7 int flag[100] = {0}; 8 int n,c;//数量,容量 9 int w[100],v[100];//重量,价值 10 int max = 0;//最大价值 11 int b[100] = {0}; 12 int num = 0; 13 void f(int weight,int val); 14 int main(){ 15 scanf("%d %d",&n,&c); 16 for(int i=0;i<n;i++){ 17 scanf("%d",&w[i]); 18 } 19 for(int i=0;i<n;i++){ 20 scanf("%d",&v[i]); 21 } 22 f(0,0); 23 printf("最大价值为:%d ",max); 24 } 25 void f(int weight,int val){ 26 for(int i=0;i<n;i++){ 27 if(flag[i]==0){ 28 if(weight+w[i]<=c){ 29 flag[i] = 1; 30 f(weight+w[i],val+v[i]); 31 flag[i] = 0; 32 }else{ 33 max = max>val?max:val; 34 } 35 } 36 } 37 }
测试结果:
3 6
1 3 5
4 3 6
对于动态规划:
首先定义状态:方法f(i,j)或者矩阵a[i][i]表示前i个包(阶段)容量为j时(状态)计算出最大价值。
状态转移方程:f(i,j) = max(a[i-1][j],v[i]+f(i-1,j-w[i]))。
找到状态转移方程就可以解决动态规划问题,可以使用递归也可以使用递推:
递归代码:
1 #include<stdio.h> 2 /*测试结果44 3 4 20 4 8 7 6 4 5 14 15 20 9 6 */ 7 int n,c;//数量,容量 8 int w[100],v[100];//重量,价值 9 int max = 0;//最大价值 10 int a[100][100]= {0}; 11 int f(int i,int j); 12 //int num = 0; 13 int main(){ 14 scanf("%d %d",&n,&c); 15 for(int i=1;i<=n;i++){ 16 scanf("%d",&w[i]); 17 } 18 for(int i=1;i<=n;i++){ 19 scanf("%d",&v[i]); 20 } 21 max = f(n,c); 22 printf("%d ",max); 23 //printf("递归调用%d次 ",num); 24 printf("递归记录矩阵: "); 25 for(int i=0;i<c;i++){ 26 if(i==0) printf(" "); 27 printf("%3d",i+1); 28 } 29 printf(" "); 30 for(int i=1;i<=n;i++){ 31 for(int j = 1;j<=c;j++){ 32 if(j==1) printf("%3d",i); 33 printf("%3d",a[i][j]); 34 } 35 printf(" "); 36 } 37 } 38 int f(int i,int j){ 39 //num++; 40 if(j-w[i]<0){ 41 return f(i-1,j); 42 }else if(i==1){ 43 a[i][j] = v[1]; 44 return v[1]; 45 }else if(i==0){ 46 return 0; 47 } 48 int num1,num2; 49 if(a[i-1][j]!=0) num1=a[i-1][j]; 50 else num1 = f(i-1,j); 51 if(a[i-1][j-w[i]]!=0) num2=a[i-1][j-w[i]]; 52 else num2 = v[i]+f(i-1,j-w[i]); 53 a[i][j] = num1>num2?num1:num2; 54 return a[i][j]; 55 }
递推代码
1 /*测试结果10 2 3 6 3 1 3 5 4 4 3 6 5 */ 6 #include<stdio.h> 7 int n,c;//数量,容量 8 int w[100],v[100];//重量,价值 9 int a[100][100]= {0}; 10 int main(){ 11 scanf("%d %d",&n,&c); 12 for(int i=1;i<=n;i++){ 13 scanf("%d",&w[i]); 14 } 15 for(int i=1;i<=n;i++){ 16 scanf("%d",&v[i]); 17 } 18 for(int i=1;i<=n;i++){ 19 for(int j=1;j<=c;j++){ 20 if(j-w[i]>=0) 21 a[i][j] = a[i-1][j]>v[i]+a[i-1][j-w[i]]?a[i-1][j]:v[i]+a[i-1][j-w[i]]; 22 else 23 a[i][j] = a[i-1][j]; 24 } 25 } 26 printf("%d ",a[n][c]); 27 printf("递推记录矩阵: "); 28 for(int i=0;i<c;i++){ 29 if(i==0) printf(" "); 30 printf("%3d",i+1); 31 } 32 printf(" "); 33 for(int i=1;i<=n;i++){ 34 for(int j = 1;j<=c;j++){ 35 if(j==1) printf("%3d",i); 36 printf("%3d",a[i][j]); 37 } 38 printf(" "); 39 } 40 41 }
测试结果:
对于测试用例
3 6
1 3 5
4 3 6
可以看出两种方法本质上是一样,递推记录了每一种容量状态,递归记录了需要的容量状态。
总结:
- 动态规划的本质是对问题状态的定义和状态转移方程的定义。
- 找状态转移方程需用到分治的思想,找到最优子结构。
- 找到状态转移方程便可以使用递归或者递推方法解决问题。