动态规划题解
1. DP专题
适用问题
回溯适用的问题:穷举;
动态规划:回溯+存在重复子问题;
案例1 0-1背包问题
0-1背包问题:对于一组不同重量的物品,选择其中一些物品装入背包,在不超过背包最大重量限制的前提下,背包中可装物品总重量的最大值是多少?
物品重量:{2, 2, 4, 6, 3}
背包最大承重: 9
这个问题可以用回溯来解决,回溯的关键是要构建 多阶段决策模型:每一阶段决策一个物品是否装入背包
n个物品对应n个阶段,每个阶段决定1个物品是否要装入背包,这组决策对应物品重量是否是最大值,对比下看看背包的物品是否是最大值;
//1.回溯解法 private int maxW = Integer.MIN_VALUE; //存储背包中物品总重量的最大值,结果放到maxW中 private int[] weight = {2,2,4,6,3}; //物品重量 private int n = 5; //物品个数 private int w = 9; //背包承受的最大重量 private void f(int i, int cw) { //调用f(0,0),i表示决策的物品,cw前面决策完成之后背包的物品重量; (0, i-1) if (cw == w || i == n) { //中间决策的可行性解; if (cw > maxW) maxW = cw; return; } f(i+1,cw);//选择不装第i个物品 if (cw + weight[i] <= w) { f(i+1,cw+weight[i]); //选择装第i个物品 } }
递归树 != 决策树
在决策第二个物品时(重量为2)它的状态有重复的 f(2, 2)、f(2, 2) ,没必要这两个都往下递归,保留一个往下递归即可;其他同理;类似斐波那契数列的重复子问题;
采用备忘录的方法去解决重复子问题;
定义一个二维数组mem: (n, w+1) 即 (5个物品, 背包重量w+1)
从性能上来说备忘录的解法和动态规划的一样的了,但是不是所有的问题都可以用备忘录的解法去去重;
//2.回溯解法: 使用备忘录解决重复子问题 private boolean[][] mem = new boolean[5][10]; //备忘录,默认值false public void f_2(int i, int cw) { //调用f_2(0,0) if(cw == w || i == n) { //cw==w表示装满了,i==n表示物品都考察完了 if (cw > maxW) maxW = cw; return; } if (mem[i][cw]) return; //重复状态 mem[i][cw] = true; //记录(i,cw)这个状态 f_2(i+1, cw); //选择不装第i个物品 if (cw + weight[i] <= w) { f_2(i+1, cw+weight[i]); //选择装第i个物品 } }
动态规划解法
构建多阶段决策模型。把每一层(每一阶段)重复的状态合并,只记录不同的状态,然后基于上一层(上一阶段)的状态集合,来推导下一层(下一阶段)的状态集合。
(i, cw) 表示一个状态,表示第i 个物品决策完之后背包重量为cw,cw的取值为0~w(w表示背包的承载重量) 。
通过合并每一层重复的状态,这样就能保证每一层的状态个数不会超过w个(w表示背包的承载重量),也就是例子中的9;
于是我们就可以避免回溯算法递归树中每层状态个数的指数级增长。
物品重量:{2, 2, 4, 6, 3}
背包最大承重: 9
f(i, cw) 表示第i 个物品将要决策,此时背包重量cw;
dp[i][cw] 表示第i个物品决策完,此时背包重量cw;
2 0 2
2 0 2 4
4 0 2 4 6 8
6 0 2 4 6 8
3 0 2 3 4 6 7 8 9
如何记录每一层不重复的状态,可以用hashset,或者一一映射的布尔类型的数组;
物品总重量不会超过w,我们用一个boolean类型的二维数组dp[n][w+1]来记录每层可以达到的不同状态。
第0个物品的重量是2,要么装入背包,要么不装入背包,决策完之后,对应背包的状态有2种: 背包中物品的总重量是0或者2;
我们用dp[0][0]=true和 dp[0][2]=true来表示这两种状态;
第1个物品的重量也是2,在第0个物品决策完之后,背包中物品的重量有2种情况:0或者2;基于第0个物品决策完之后的状态,在第1个物品决策完之后,对应背包中物品总重量分别有三种情况:
0(0+0),2(0+2 or 2+0),4(2+2) ;我们用dp[1][0] = true,dp[1][2] = true, dp[1][4] = true来表示这三种状态。
以此类推,基于第i个物品决策完之后的状态来推导第i+1个物品决策完之后的状态。
2 0 2
2 0 2 4 (0 2 2 4)
4 0 2 4 6 8
6 0 2 4 6 8
3 0 2 3 4 6 7 8 9
对应的dp二维数组见下,状态转移方程,基于上层来填写下一层:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
2 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
3 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
4 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 1 |
//3.动态规划解法 public int knapsack(int[] weight, int n, int w) { boolean[][] dp = new boolean[n][w+1]; //默认值false dp[n][w+1]记录每层达到的不同状态 dp[0][0] = true; //初始化 weight{2,2,4,6,3} if (weight[0] <= w) { dp[0][weight[0]] = true;//第0个物品的重量是2,要么装入背包要么不装,对应的背包状态有2种: 0或者2, 即dp[0][0]=true, dp[0][2]=true } //第1个物品的重量是2,要么装入背包要么不装,对应的背包状态有3种: 0/2/4, 即dp[1][0]=true, dp[1][2]=true, dp[1][4]=true for (int i = 1; i < n; i++) { //动态规划状态转移 for (int j = 0; j <= w; j++) { //遍历第i-1个阶段的所有可达状态 if (dp[i-1][j] == true) { //判断i-1层状态是否可达,基于上层状态可达推导下一个阶段状态 dp[i][j] = true; //推导第i层状态 if (j+weight[i] <= w) { dp[i][j+weight[i]] = true; //推导第i层状态 } } } } for (int i = w; i >= 0; i--) { //输出结果,把dp填完之后,把为true的从后往前扫描一遍,输出可达的最大背包重量(即最后一个); if (dp[n-1][i] == true) return i; } return 0; }
案例2 二维费用背包问题
对于一组不同重量、不同价值、不可分割的物品,选择将其中某些物品转入背包,在不超过背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少?
注意这里是求最大总价值,而非最大总重量。
物品重量: {2, 2, 4, 6, 3}
物品价值: {3, 4, 8, 9, 6}
背包最大承重: 9
//1.回溯法,无法用备忘录去实现 i,cw相同的情况下取max(cv) private int maxW = Integer.MIN_VALUE; //存储背包中物品总重量的最大值,结果放到maxW中 private int[] weight = {2,2,4,6,3}; //物品重量 private int[] value = {3,4,8,9,6}; private int n = 5; //物品个数 private int w = 9; //背包承受的最大重量 private void f(int i, int cw, int cv) { //调用f(0,0,0), i表示阶段、 cw表可达的物品总重量、 cv表可达的物品总价值 if (cw == w || i == n) { //cw==w表示装满了,i==n表示物品都考察完了 if (cv > maxW) maxW = cv; return; } f(i+1,cw,cv);//选择不装第i个物品 if (cw + weight[i] <= w) { f(i+1,cw+weight[i], cv+value[i]); //选择装第i个物品 } }
这类就无法使用备忘录去重了,因为相同重量它的总价值可能是不一样的,无法使用mem[n][cw] 去记录;
在第2个物品时,虽然没有重复的,但是f(2, 2, 4) 的总价值 大于 f(2, 2, 3)的总价值,就没必要保留它这个状态往下递归了;
动态规划的解法
多阶段决策模型,每个阶段决策完之后,背包中的物品的总重量以及总价值,会有多种情况,也就是会达到多种不同的状态。
(i, cw, cv) 表示一个状态,第i 个物品决策完之后,背包重量为cw,对应的最大价值为cv;
用一个二维数组dp[n][w+1] ,来记录每个阶段可以达到的状态。dp[ i ][ j ]表示第i 个物品决策完之后,背包重量为j ,对应的最大价值。
把递归树每一层中(i, cw)重复的状态(节点)合并,只记录cv值最大的那个状态,然后基于这些状态来推导下一层(下一个阶段)的状态。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
0 | 0 | -1 | 3 | -1 | -1 | -1 | -1 | -1 | -1 | -1 |
1 | 0 | -1 | 4 | -1 | -1 | 7 | -1 | -1 | -1 | -1 |
2 | ||||||||||
3 | ||||||||||
4 |
//2.动态规划解法 {2, 2, 4, 6, 3}, {3, 4, 8, 9, 6}, 5, 9 //二维数组dp[n][w+1]记录每个阶段可以达到的状态, dp[i][j]表示第i个物品决策完之后,背包重量为j,对应的最大价值 public int knapsack_2(int[] weight, int[] value, int n, int w) { int[][] dp = new int[n][w+1]; for (int i = 0; i < n; i++) { for (int j = 0; j <= w; j++) { dp[i][j] = Integer.MIN_VALUE;//默认最小值,为状态不可达 } } dp[0][0] = 0; //dp[0][0] = 0表示状态可达的价值为0, i表示阶段, j表示物品重量cw, dp[i][j]表示最大价值cv if (weight[0] <= w) { dp[0][weight[0]] = value[0]; } for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if (dp[i-1][j] == Integer.MIN_VALUE) { //判断状态是否可达 continue; } dp[i][j] = Math.max(dp[i][j], dp[i-1][j]); //可达(不放进背包), 比较上一个可达状态最大价值和 当前层可达状态最大价值 的大小 if (j+weight[i] <= w) { dp[i][j+weight[i]] = Math.max(dp[i][j+weight[i]], dp[i-1][j] + value[i]);//可达(放进背包) } } } int res = Integer.MIN_VALUE; for (int j = 0; j <= w; j++) { //扫描最后一行, 输出结果 if (res < dp[n-1][j]) { res = dp[n-1][j]; } } return res; }
总结
回溯 + 存在重复子问题/ 重复状态 ==>> 动态规划解决
解题步骤
- 可用回溯解决:需要穷举搜索才能得到结果的问题(最值、可行、计数等);
- 构建多阶段决策模型:看是否能将问题求解的过程分为多个阶段;
- 查看是否存在重复子问题:是否有多个路径到达同一个状态;
- 定义状态:也就是如何记录每一阶段的不重复状态;
- 定义状态转移方程:也就是找到如何通过上一阶段的状态推导下一下阶段的状态;
- 画状态转移表:辅助理解,验证正确性,确定状态转移的初始值;
- 编写动态规划代码;
4、5两个步骤是难点,掌握的技巧是:记忆经典模型的状态和状态转移方程的定义方法,举一反三。
0-1背包问题:对于一组不同重量的物品,选择其中一些物品装入背包,在不超过背包最大重量限制的前提下,背包中可装物品总重量的最大值是多少?
- 1. 可用回溯解决,穷举问题;
- 2. 构建多阶段决策模型:每一阶段决策一个物品是否放入背包;
- 3. 查看是否存在重复子问题:某一阶段背包中物品重量为cw,可以通过不同路径到达;
- 4. 定义状态
- boolean dp[n][w+1] 记录每一阶段可达的所有状态;
- dp[i][j] = true表示第i 个物品决策完之后,存在背包中物品重量为j 这种状态;
- 5. 定义状态转移方程:
- 确定第i 阶段的(i, j) 这个状态,如何通过上一个阶段i - 1的哪些状态转移过来
- (i, j)这个状态只有可能由(i-1, j)和 (i-1, j-weight[i]) 转移过来
- dp[i][j] = dp[i-1][j] || dp[i-1] [j - weight[i]] ;
- 6. 画状态转移表: 辅助理解,验证正确性,确定状态转移的初始值。
- 7. 编写动态规划代码
public int knapsack2(int[] weight, int n, int w) { boolean[][] dp = new boolean[n][w+1]; //默认值false; 状态 dp[0][0] = true; //初始化 if (weight[0] <= w) { dp[0][weight[0]] = true; } //动态规划状态转移 //正向推 i -> i+1 for (int i = 1; i < n; i++) { //动态规划状态转移 for (int j = 0; j <= w; j++) { //遍历第i-1个阶段的所有可达状态 if (dp[i-1][j] == true) { //可达 dp[i][j] = true; //推导第i层状态 if (j+weight[i] <= w) { dp[i][j+weight[i]] = true; //推导第i层状态 } } } } //反向推 i-1 -> i ,标准的状态方程写法 //dp[i][j] = dp[i-1][j] || dp[i-1][j-weight[i]] for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if (dp[i-1][j] == true || (j-weight[i] >= 0 && dp[i-1][j-weight[i]] == true)) { dp[i][j] = true; } } } for (int i = w; i >= 0; i--) { //输出结果 if (dp[n-1][i] == true) return i; } return 0; }
二维费用背包:
对于一组不同重量、不同价值、不可分割的物品,选择将其中某些物品转入背包,在不超过背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少?
- 1. 可用回溯解决,穷举问题;
- 2. 构建多阶段决策模型:每一阶段决策一个物品是否放入背包;
- 3. 查看是否存在重复子问题:某一阶段背包中物品重量为cw,可以通过不同路径到达;
- 4. 定义状态
- int dp[n][w+1] 记录每一个阶段可达的所有状态;
- dp[i][j] 表示第i 个物品决策完之后,存在背包中物品重量为 j ,对应的最大物品价值;
- 5. 定义状态转移方程:
- 确定第i 阶段的( i, j ) 这个状态,如何通过上一个阶段 i - 1的哪些状态转移过来;
- (i, j)这个状态只有可能由(i-1, j)和 (i-1, j-weight[i]) 转移过来;
- dp[i][j] = Math.max(dp[i-1][j], dp[i-1] [j - weight[i]] + value[i] ) ;
- 6. 画状态转移表: 辅助理解,验证正确性,确定状态转移的初始值。
- 7. 编写动态规划代码
public int knapsack_22(int[] weight, int[] value, int n, int w) { int[][] dp = new int[n][w+1]; for (int i = 0; i < n; i++) { for (int j = 0; j <= w; j++) { dp[i][j] = Integer.MIN_VALUE; } } dp[0][0] = 0; if (weight[0] <= w) { dp[0][weight[0]] = value[0]; } //正推 i -> i+1 for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if (dp[i-1][j] == Integer.MIN_VALUE) { continue; } dp[i][j] = Math.max(dp[i][j], dp[i-1][j]); if (j+weight[i] <= w) { dp[i][j+weight[i]] = Math.max(dp[i][j+weight[i]], dp[i-1][j] + value[i]); } } } //逆推 for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if (dp[i-1][j] != Integer.MIN_VALUE) { dp[i][j] = Math.max(dp[i][j], dp[i-1][j]); } if (j-weight[i] >= 0 && dp[i-1][j-weight[i]] != Integer.MIN_VALUE) { dp[i][j] = Math.max(dp[i][j], dp[i-1][j-weight[i]]+ value[i]); } } } int res = Integer.MIN_VALUE; for (int j = 0; j <= w; j++) { //输出结果 if (res < dp[n-1][j]) { res = dp[n-1][j]; } } return res; }
最值、可行、计数三种类型
一些特殊小类别:树形DP(线性DP和树形DP)、区间DP、数位DP
- 1. 有n个物品,选择其中一些物品装入背包,在不超过背包最大重量限制的前提下,背包中可装物品总重量的最大值是多少? (最值)
- 2. 有n个物品,选择其中一些物品装入背包,能不能正好装满背包?(可行)
- 3. 有n个物品,选择其中一些物品装入背包,正好装满背包所需物品最少个数?(如果装不满,返回-1) (最值)
- 4. 有n个物品,选择其中一些物品装入背包,装满背包有多少种不同的装法?(计数)
背包问题都可以对应以上四种问法,0-1、完全、多重和 二维费用;
1、有n个物品,选择其中一些物品装入背包,在不超过背包最大重量限制的前提下,背包中可装物品总重量的最大值是多少? (最值)
状态:
- boolean dp[n] [w+1] 记录每阶段可达状态;
- dp[i] [j] = true表示第i 个物品决策完之后背包重量为 j 这个状态可达;
状态转移方程:
- (i, j) 这个状态只有可能从(i-1, j) 和(i-1, j-weight[i] ) 两个状态转移过来;
- dp[i][j] = dp[i-1] [j] || dp[i-1][j - weight[i]]
2. 有n个物品,选择其中一些物品装入背包,能不能正好装满背包 (可行)
状态:
- boolean dp[n] [w+1] 记录每阶段可达状态;
- dp[i] [j] = true表示第i 个物品决策完之后背包重量为 j 这个状态可达;
状态转移方程:
- (i, j) 这个状态只有可能从(i-1, j) 和(i-1, j-weight[i] ) 两个状态转移过来;
- dp[i][j] = dp[i-1] [j] || dp[i-1][j - weight[i]]
public int knapsack2(int[] weight, int n, int w) { //可行 boolean[][] dp = new boolean[n][w+1]; //默认值false; 状态 dp[0][0] = true; //初始化 if (weight[0] <= w) { dp[0][weight[0]] = true; } //动态规划状态转移 for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if (dp[i-1][j] == true || (j-weight[i] >= 0 && dp[i-1][j-weight[i]] == true)) { dp[i][j] = true; } } } return dp[n-1][w]; }
3. 有n个物品,选择其中一些物品装入背包,正好装满背包所需物品最少个数?(如果装不满,返回-1) (可行)
状态:
- int dp[n] [w+1] 记录每阶段可达重量对应的最少物品个数;
- dp[i] [j] = true表示第i 个物品决策完之后,背包重量为 j,对应的最少物品个数;
状态转移方程:
- (i, j) 这个状态只有可能从(i-1, j) 和(i-1, j-weight[i] ) 两个状态转移过来;
- dp[i][j] = Math.min(dp[i-1] [j], dp[i-1][j - weight[i]] + 1)
public int knapsack3(int[] weight, int n, int w) { //最值 int[][] dp = new int[n][w+1]; //记录到达某个状态,最少物品数量 for (int i = 0; i < n; i++) { for (int j = 0; j <= w; j++) { dp[i][j] = Integer.MAX_VALUE; } } dp[0][0] = true; //初始化 if (weight[0] <= w) { dp[0][weight[0]] = 1; } //动态规划状态转移 for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if ([j-weight[i]] < 0)) { dp[i][j] = dp[i-1][j]; } else { dp[i][j] = Math.min(dp[i-1][j], dp[i-1][j-weight[i]] + 1) } } } if (dp[n-1][w] == Integer.MAX_VALUE) return -1; return dp[n-1][w]; }
4. 有n个物品,选择其中一些物品装入背包,装满背包有多少种不同的装法?(计数)
状态:
- int dp[n] [w+1] 记录每阶段可达重量对应的装法个数;
- dp[i] [j] = true表示第i 个物品决策完之后,背包重量为 j,对应有几种装法;
状态转移方程:
- (i, j) 这个状态只有可能从(i-1, j) 和(i-1, j-weight[i] ) 两个状态转移过来;
- dp[i][j] = dp[i-1] [j] + dp[i-1][j - weight[i]]
public int knapsack4(int[] weight, int n, int w) { //计数 int dp = new int[n][w+1]; //记录到达某个状态有几条路径 dp[0][0] = 1; //初始化 if (weight[0] <= w) { dp[0][weight[0]] = 1; } //动态规划状态转移 for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if ([j-weight[i]] < 0)) { dp[i][j] = dp[i-1][j]; } else { dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]]; } } } return dp[n-1][w]; }
空间优化
对于一组不同重量、不可分割的物品,选择其中一些物品装入背包,能不能正好装满背包?
物品重量:{2, 2, 4, 6, 3}
背包最大承重: 9
状态:
- boolean dp[n] [w+1] 记录每阶段可达状态;
- dp[i] [j] = true表示第i 个物品决策完之后,背包重量为 j 这个状态可达;
状态转移方程:
- (i, j) 这个状态只有可能从(i-1, j) 和(i-1, j-weight[i] ) 两个状态转移过来;
- dp[i][j] = dp[i-1] [j] || dp[i-1][j - weight[i]]
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
2 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
3 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
4 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
状态转移只用 i-1 和i 行,滚动的去利用;
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
0 | ||||||||||
1 |
//二维数组 public int knapsack2(int[] weight, int n, int w) { //可行 boolean[][] dp = new boolean[n][w+1]; //默认值false; dp[0][0] = true; //初始化 if (weight[0] <= w) { dp[0][weight[0]] = true; } //动态规划状态转移 for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if (dp[i-1][j] == true || (j-weight[i] >= 0 && dp[i-1][j-weight[i]] == true)) { dp[i][j] = true; } } } return dp[n-1][w]; } //滚动数组 public int knapsack2_mem(int[] weight, int n, int w) { //可行 boolean[][] dp = new boolean[2][w+1]; //默认值false; dp[0][0] = true; if (weight[0] <= w) { dp[0][weight[0]] = true; } int turn=1; //该填充第turn行了(trun=0或1) //动态规划状态转移 for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if (dp[(turn+1)%2][j] == true || (j-weight[i] >= 0 && dp[(turn+1)%2][j-weight[i]] == true)) { dp[turn][j] = true; } } turn = (turn+1)%2; //0变为1, 或者1变为0 } return dp[(turn+1)%2][w]; }
一维数组
dp[i][j] = dp[i-1][j] || dp[i-1][j - weight[i]]
public int knapsack2_mem(int[] weight, int n, int w) { boolean[][] dp = new boolean[n][w+1]; //默认值false; dp[0] = true; //初始化 if (weight[0] <= w) { dp[weight[0]] = true; } //动态规划状态转移 for (int i = 1; i < n; i++) { for (int j = w; j >= 0; j--) { if (dp[j] == true || (j-weight[i] >= 0 && dp[j-weight[i]])) { dp[j] = true; } } } return dp[w]; }
2. 经典模型
背包问题
(0-1、完全、多重、二维费用、分组、有依赖的)
1)0-1背包问题:
有n个物品,重量分别为weight[i] (i=0~n-1)每个物品只有一个,选择其中一些物品装入背包,
在不超过背包总重量w的前提下,...
2)完全背包问题
有n个物品,重量分别为weight[i] (i=0~n-1),每个物品有无限多个,选择一些物品装入背包,
在不超过背包总重量w的前提下...
3)多重背包问题
有n个物品,重量分别为weight[i] (i=0~n-1),每个物品有有限多个,个数分别为count[i] (i=0~n-1),
选择一些物品装入背包, 在不超过背包重量w的前提下...
4)二维费用
有n个物品,重量分别为weight[i] (i=0~n-1), 价值分别为value[i] (i=0~n-1), 在不超过背包重量的前提下,
装入背包物品的最大价值是多少?
要求解的问题:
- a)背包可装物品总重量的最大值是多少?
- b)是否能装满整个背包?
- c)正好装满背包最少需要多少物品?
- d)装满背包有多少种装法?
把所有解都罗列出来不能用dp来求解,dp只适合最优、计数可行,所有解罗列出适合回溯 如排列组合等;
0-1背包问题
有n个物品,重量分别为weight[i] (i=0~n-1)每个物品只有一个, 选择其中一些物品装入背包, 在不超过背包总重量w的前提下, ...
0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个
状态:
boolean dp[n][w+1] 记录每阶段可达状态;
dp[i][j] = true 表示第i 个物品决策完之后背包重量为j 这个状态可达;
状态转移方程:
(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来
dp[i][j] = dp[i-1][j] || dp[i-1][j-weight[i]]
要求解的问题:
- a)背包可装物品总重量的最大值是多少?
public int knapsack1(int[] weight, int n, int w) { boolean[][] dp = new boolean[n][w+1]; //默认值false; 状态 dp[0][0] = true; //初始化 if (weight[0] <= w) { dp[0][weight[0]] = true; } //动态规划状态转移 //反向推 i-1 -> i ,标准的状态方程写法 //dp[i][j] = dp[i-1][j] || dp[i-1][j-weight[i]] for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if (dp[i-1][j] == true || (j-weight[i] >= 0 && dp[i-1][j-weight[i]] == true)) { dp[i][j] = true; } } } for (int i = w; i >= 0; i--) { //输出结果 if (dp[n-1][i] == true) return i; } return 0; }
要求解的问题:
- b)是否能装满整个背包?
背景和要求解的问题 a)背包可装物品总重量的最大值是多少? 是一样的, 解法是很类似的,只是最后返回的结果不一样;
看最后dp[n-1][w] 是否为true,为true说明是可达的,背包的总重量可以是w,如果为fale 状态不可达 背包总重量不可以达到w;
public int knapsack2(int[] weight, int n, int w) { boolean[][] dp = new boolean[n][w+1]; //默认值false; 状态 dp[0][0] = true; //初始化 if (weight[0] <= w) { dp[0][weight[0]] = true; } //动态规划状态转移 for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if (dp[i-1][j] == true || (j-weight[i] >= 0 && dp[i-1][j-weight[i]] == true)) { dp[i][j] = true; } } } return dp[n-1][w]; }
要求解的问题:
- c)正好装满背包最少需要多少物品?
0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个
状态:
int dp[n][w+1] 记录每阶段可达重量对应的最少物品个数;
dp[i][j] = true 表示第i 个物品决策完之后,背包重量为j,对应的最少物品个数;
状态转移方程:
(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来
dp[i][j] = Math.min(dp[i-1][j], dp[i-1][j-weight[i]]+1 )
最值问题, (i, j)不同路径到达的个数不一样;
public int knapsack3(int[] weight, int n, int w) { //最值 int[][] dp = new int[n][w+1]; //记录到达某个状态,最少物品数量 for (int i = 0; i < n; i++) { for (int j = 0; j <= w; j++) { dp[i][j] = Integer.MAX_VALUE; } } dp[0][0] = true; //初始化 if (weight[0] <= w) { dp[0][weight[0]] = 1; } //动态规划状态转移 for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if ([j-weight[i]] < 0)) { dp[i][j] = dp[i-1][j]; } else { dp[i][j] = Math.min(dp[i-1][j], dp[i-1][j-weight[i]] + 1) //这里会有越界问题 } } } if (dp[n-1][w] == Integer.MAX_VALUE) return -1; return dp[n-1][w]; }
要求解的问题:
- d)装满背包有多少种装法?
0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个
状态:
int dp[n][w+1] 记录每阶段可达重量对应的最少装法个数;
dp[i][j] 表示第i 个物品决策完之后,背包重量为j,对应有几种装法;
状态转移方程:
(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来
dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]]
public int knapsack4(int[] weight, int n, int w) { //计数 int dp = new int[n][w+1]; //记录到达某个状态有几条路径 dp[0][0] = 1; //初始化 if (weight[0] <= w) { dp[0][weight[0]] = 1; } //动态规划状态转移 for (int i = 1; i < n; i++) { for (int j = 0; j <= w; j++) { if ([j-weight[i]] < 0)) { dp[i][j] = dp[i-1][j]; } else { dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]]; } } } return dp[n-1][w]; }
完全背包问题
有n个物品,重量分别为weight[i] (i=0~n-1),每个物品有无限多个, 选择一些物品装入背包, 在不超过背包总重量w的前提下...
0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个
状态:
boolean dp[n][w+1] 记录每阶段可达状态;
dp[i][j] = true 表示第i 个物品决策完之后背包重量为j 这个状态可达;
状态转移方程:
(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来
dp[i][j] = dp[i-1][j] || dp[i-1][j-weight[i]]
类比:
完全背包: 每个阶段决策一个物品怎么装入背包,0个、1个...k个
状态: 不变
状态转移方程:
(i, j) 这个状态可能从(i-1, j)、 (i-1, j-weight[i])、(i-1, j-2*weight[i])、...、(i-1, j-k*weight[i]) 转移过来;
dp[i][j] = dp[i-1][j] || dp[i-1][j-weight[i]] || dp[i-1][j-2*weight[i]] || ...|| dp[i-1][j-k*weight[i]]
要求解的问题:
- a)背包可装物品总重量的最大值是多少?
- b)是否能装满整个背包?
- c)正好装满背包最少需要多少物品?
- d)装满背包有多少种装法?
//完全背包-1 : 背包重量w的限制下最大可装物品重量 public int wanquan_1(int[] weight, int n, int w) { boolean[][] dp = new boolean[n][w+1]; //默认false for (int c = 0; c <= w/weight[0]; ++c) { //最大装 w/k, w/weight[0] dp[0][c*weight[0]] = true; } for (int i = 1; i < n; ++i) { for (int j = 0; j <= w; ++j) { //(i, j) int k = j/weight[i]; for (int c = 0; c <= k; ++c) { if (dp[i-1][j-c*weight[i]]) { dp[i][j] = true; break; } } } } for (int i = w; i >= 0; --i) { if (dp[n-1][i] == true) return i; } return 0; }
要求解的问题:
- b)是否能装满整个背包?
//完全背包-2 : 是否能装满背包 public boolean wanquan_2(int[] weight, int n, int w) { boolean[][] dp = new boolean[n][w+1]; //默认false for (int c = 0; c <= w/weight[0]; ++c) { //最大装 w/k, w/weight[0] dp[0][c*weight[0]] = true; } for (int i = 1; i < n; ++i) { for (int j = 0; j <= w; ++j) { //(i, j) int k = j/weight[i]; for (int c = 0; c <= k; ++c) { if (dp[i-1][j-c*weight[i]]) { dp[i][j] = true; break; } } } } return dp[n-1][w]; }
2) 完全背包问题
要求解的问题:
- c)正好装满背包最少需要多少物品?
0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个
状态:
int dp[n][w+1] 记录每阶段可达重量对应的最少物品个数;
dp[i][j] 表示第i 个物品决策完之后,背包重量为j,对应的最少物品个数;
状态转移方程:
(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来
dp[i][j] = Math.min(dp[i-1][j], dp[i-1][j-weight[i]]+1 )
完全背包: 每个阶段决策一个物品怎么装入背包,0个、1个...k个
状态: 不变
状态转移方程:
(i, j) 这个状态可能从(i-1, j)、 (i-1, j-weight[i])、(i-1, j-2*weight[i])、...、(i-1, j-k*weight[i]) 转移过来;
dp[i][j] = Math.min( dp[i-1][j], dp[i-1][j-weight[i]]+1, dp[i-1][j-2*weight[i]] + 2, ..., dp[i-1][j-k*weight[i]] + k )
//完全背包-3 : 装满背包所需最少物品个数 public int wanquan_3(int[] weight, int n, int w) { int[][] dp = new int[n][w+1]; //默认false for (int i = 0; i < n; ++i) { for (int j = 0; j <= w; j++) { dp[i][j] = Integer.MAX_VALUE; } } for (int c = 0; c <= w/weight[0]; c++) { dp[0][c*weight[0]] = c; } for (int i = 1; i < n; ++i) { for (int j = 0; j <= w; ++j) { //(i, j) int k = j/weight[i]; for (int c = 0; c <= k; ++c) { if (dp[i-1][j-c*weight[i]] != Integer.MAX_VALUE && dp[i-1][j-c*weight[i]] + c < dp[i][j]) { dp[i][j] = dp[i-1][j-c*weight[i]] + c; } } } } return dp[n-1][w]; }
- d)装满背包有多少种装法?
0-1背包: 每个阶段决策一个物品是否装入背包,0个或1个
状态:
int dp[n][w+1] 记录每阶段可达重量对应的装法个数;
dp[i][j] 表示第i 个物品决策完之后,背包重量为j,对应有几种装法;
状态转移方程:
(i, j) 这个状态只有可能从(i - 1, j) 和 (i-1, j-weight[i]) 两个状态转移过来
dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]]
完全背包: 每个阶段决策一个物品怎么装入背包,0个、1个...k个
状态: 不变
状态转移方程:
(i, j) 这个状态可能从(i-1, j)、 (i-1, j-weight[i])、(i-1, j-2*weight[i])、...、(i-1, j-k*weight[i]) 转移过来;
dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]] + dp[i-1][j-2*weight[i]] + dp[i-1][j-k*weight[i]]
//完全背包-4 : 装满背包总共有多少种装法 public int wanquan_4(int[] weight, int n, int w) { int[][] dp = new int[n][w+1]; //默认false for (int c = 0; c <= w/weight[0]; c++) { dp[0][c*weight[0]] = 1; } for (int i = 1; i < n; ++i) { for (int j = 0; j <= w; ++j) { //(i, j) int k = j/weight[i]; for (int c = 0; c <= k; ++c) { dp[i][j] += dp[i-1][j-c*weight[i]]; } } } return dp[n-1][w]; }
多重背包问题
有n个物品,重量分别为weight[i] (i = 0~n-1) ,每个物品有有限多个,个数分别为count[i] (i = 0~n-1),选择一些物品装入背包,在不超过背包重量w的前提下...
完全背包: 每个阶段决策一个物品怎么装入背包,0个、1个...k个
状态:
boolean dp[n][w+1] 记录每阶段可达状态;
dp[i][j] = true,表示第i 个物品决策完之后背包重量为j 这个状态可达;
状态转移方程:
(i, j) 这个状态可能从(i-1, j)、 (i-1, j-weight[i])、(i-1, j-2*weight[i])、...、(i-1, j-k*weight[i]) 转移过来;
dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]] + dp[i-1][j-2*weight[i]] + dp[i-1][j-k*weight[i]]
要求解的问题:
- a)背包可装物品总重量的最大值是多少?
- b)是否能装满整个背包?
- c)正好装满背包最少需要多少物品?
- d)装满背包有多少种装法?
多重背包问题和 完全背包的区别在于, 多重背包多了个 c <= Math.min(w/weight[0], count[0])
路径问题
打家劫舍&股票买卖
爬楼梯问题
匹配问题(LCS、编辑距离)
其他(LIS)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人