1. 动态规划
1.1 九类背包问题
1.1.1 01背包
-
问题: 有n件物品和容量为m的背包,给出每件物品的重量以及价值,且每种物品只有一个,求解让装入背包的物品重量不超过背包容量且价值最大的装法及总价值 。
-
分析:
计算最大价值:对于容量为v,且当正打算放第i件物品时,有三种情况
-
容量v比第i件物品的重量小,放不下,只能不放第i件物品,此时和容量v,放第i-1件物品的情况一样,满足
-
容量v比第i件物品的重量大,可以放下,但选择不放,此时也和容量v,放第i-1件物品的情况一样,满足
-
容量v比第i件物品的重量大,可以放下,选择放,此时总价值应为第i件物品的价值+容量为v-weight(i)时,放第i-1件物品的总价值一样,即
总体状态转移方程为
计算装入方案:构建路径矩阵,在每次选择放入时,路径矩阵置1。那么最后计算完成,对路径矩阵倒序遍历:
- 从n件物品,容量m处开始,以物品件数为序遍历路径矩阵,即找到容量m时,最后放入的物品;
- 当路径矩阵值为1时,即在此处放入的物品,再找到容量m-weight(i)时,最后放入的物品;
- 当找第0件物品时停止循环。
-
-
程序代码
// public static void pack01(int space, int[] weight, int[] value){ int[][] totalValue = new int[weight.length+1][space+1]; int[][] path = new int[weight.length+1][space+1]; //i是个数,j是空间 for(int i=1;i<weight.length+1;i++){ for(int j=1;j<space+1;j++){ if(weight[i-1]<=j){ if(totalValue[i-1][j]>totalValue[i-1][j-weight[i-1]]+value[i-1]){ totalValue[i][j]=totalValue[i-1][j]; }else { totalValue[i][j] = totalValue[i - 1][j - weight[i-1]] + value[i-1]; path[i][j]=1; } }else { totalValue[i][j]=totalValue[i-1][j]; } } } System.out.println("the total value is "+totalValue[weight.length][space]); for(int i=weight.length,j=space;i>=0;i--) { if (path[i][j] == 1) { System.out.println(i); j = j - weight[i-1]; } }
-
优化1
时间复杂度O(n2),空间复杂度O(n2)。
考虑到F矩阵第i次迭代时,均是和第i-1次,容量为 j 或 j - weight(i) 时的结果有关,那么如果将F构建为一个1维数组,则可以将空间复杂度降为O(n)。
但如果还按以前的方式计算,那么第i次循环计算F(j)时,F(j-weight(i -1))已经被更新为第i次的结果。所以可以考虑倒序遍历,这样新结果不会污染旧结果。
-
优化代码 时间复杂度O(n2),空间复杂度O(n)
public static void pack01(int space, int[] weight, int[] value){ //优化 int[] totalValue=new int[space+1]; int[][] path = new int[weight.length+1][space+1]; for(int i=1;i<weight.length+1;i++){ for(int j=space;j>=weight[i-1];j--){ if(weight[i-1]<=j) { if (totalValue[j] < totalValue[j - weight[i - 1]] + value[i - 1]) { totalValue[j] = totalValue[j - weight[i - 1]] + value[i - 1]; path[i][j] = 1; } } } } System.out.println("the total value is "+totalValue[space]); for(int i=weight.length,j=space;i>=0;i--) { if (path[i][j] == 1) { System.out.println(i); j = j - weight[i-1]; } } }
-
优化2
上面的解法中,F 矩阵的初始值为0,即可以理解为背包在容量为0且什么也不装时,价值为0。如果要求最后的结果是背包恰好装满,那么此时只有容量0的背包可以在什么也不装的情况下价值为0,容量为[1,2,…,m]的背包均没有合法的解。
也就是如果题目要求最后背包必须被装满时,F的初始化也改为F(0)=0,F(i)=Integer.MIN_VALUE [i=1,2,…,m]。
1.1.2 完全背包问题
-
问题:有n件物品和容量为m的背包,给出每件物品的重量以及价值,且每种物品有无数个,求解让装入背包的物品重量不超过背包容量的最大总价值 。
-
**分析:**物品有无数个,只需在01背包的基础上考虑,当放入某种物品时,应该放几个会使收益最大,即
也就是说,状态转移方程改写为
-
程序代码:
//这里其实只改变了内层循环的方向 public static void completePack(int space, int[] weight, int[] value){ int[] totalValue = new int[space+1]; for(int i=1;i<weight.length+1;i++){ for(int j=weight[i-1];j<space+1;j++){ totalValue[j] = Math.max(totalValue[j], totalValue[j - weight[i - 1]] + value[i - 1]); } } System.out.println("the total value is "+totalValue[space]); }
-
优化: 对于完全背包问题,可以看到每件物品的取值都在一个提前遇见的范围中,那么可以将其转化为01背包问题。也就是对于第i种物品,可以看成是有v/weight(i)件只能取或不取的具有相同重量、价值的物品。
1.1.3 多重背包问题
-
问题:有n件物品和容量为m的背包,给出每件物品的重量以及价值,且每种物品有M(i)个,求解让装入背包的物品重量不超过背包容量的最大总价值 。
-
分析: 思路和上面一样,转化为01背包
-
程序代码:
public static void multiplePack(int space, int[] weight, int[] value, int[] num){ int N = 0; for(int i : num){ N += i;} int[] valueTo01 = new int[N]; int[] weightTo01 = new int[N]; int flag = 0; for(int i = 0 ; i < num.length ; i++){ for(int j = 0 ; j < num[i] ; j++){ valueTo01[flag] = value[i]; weightTo01[flag] = weight[i]; flag++; } } pack01( space, weightTo01, valueTo01); }
-
问题扩展:
若问题改为“不考虑总价值,问是否可以正好填满背包”
类似问题,用n个不同的数,每种m个,是否可以累加得到K
分析: 定义dp[n*k]矩阵,dp[i][j]的值表示当用前i种物品刚好可以填满 j 容量的背包后剩余的第i种物品的个数,当值为-1时表示不可正好填满。
可以知道,当前i种物品恰好装满 j 容量时,比然可以再装满 j+w(i) 的容量。
那么,当用第i种物品,填充容量j时,有三种情况:
- dp[i-1][j]>=0,说明前i-1种物品就可以填满了,自然有dp[i][j]=m[i];
- j<w(i) 或 dp[i][j-w(i)]=-1 ,说明容量小于当前物品大小,或者容量j-w(i)时已经放不下,则有dp[i][j]=-1;
- else, dp[i][j]=dp[i][j-w(i)]-1,前i种物品刚好放满 j-w(i) 容量时,自然可以放满j容量。
程序代码
public static void isFullPack(int space, int[] weight, int[] num){ int[][] dp = new int[weight.length+1][space+1]; //初始化,零物品时只有零容量的值为0 for(int j=0;j<space+1;j++){ dp[0][j]=-1; } dp[0][0]=0; // for(int i=1;i<weight.length+1;i++){ for(int j=0;j<space+1;j++){ if(dp[i-1][j]>=0){ dp[i][j]=num[i-1]; }else if(j<weight[i-1]||dp[i][j-weight[i-1]]==-1){ dp[i][j]=-1; }else { dp[i][j]=dp[i][j-weight[i-1]]-1; } } } //如果可以恰好装满,则给出装填方法 if(dp[weight.length][space]>=0) { int temp; for (int i = weight.length, j = space; i > 0 && j >= 0; i--) { temp = num[i - 1] - dp[i][j]; System.out.println("the " + i + "th object : " + temp); j -= temp * weight[i - 1]; } } }
1.1.4 二维费用背包问题
-
问题: 对于每件可选择的物品,有两种不同的费用,每种费用都有一个可付出的最大容量,那么如何选择物品使获得的价值最大。
-
分析: 与01背包相同,只不过价值矩阵加一个维度。状态转移方程如下:
-
问题拓展: 额外的费用通常以更加隐晦的方式给出,如限制可取的最大件数,这就相当于额外增加每件的费用为1,总体解决思路还应模仿01、完全、多重背包问题上靠。
1.1.5 分组背包问题
-
问题: 所有可被选择的物品被分为K组,每组中物品互相冲突,最多选一件,该如何选择?
-
分析: 这个问题的选择策略变为:要么在这一组中选一件,要么跳过这一组,状态转移方程为
-
程序代码
/** * @param space 背包容量 * @param weight 同一行表示同一分组 * @param value 与weight对应 */ public static void groupPack(int space, int[][] weight, int[][] value){ int[] totalValue=new int[space+1]; for(int k=0;k<weight.length;k++){ for(int i=space;i>=0;i--){ for(int j=0;j<weight[k].length;j++) { if(i>weight[k][j]) { totalValue[i] = Math.max(totalValue[i], totalValue[i - weight[k][j]] + value[k][j]); } } } } System.out.println(totalValue[space]); }
-
优化:当一组中存在 i,j 两个物品,i 比 j 重,且 j 比 i 价值更高,则直接跳过 i。
1.1.6 背包问题的扩展
- 输出字典序最小的方案
- 输出方案总数
- 输出最优方案总数
- 求次优解、第K优解