背包九讲
一、01背包
1.问题描述
已知一个背包最多能容纳体积之和为V的物品。现有 n 个物品,第 i 个物品的体积为 vi , 重量为 wi。求当前背包最多能装多大重量的物品?
2.题目地址
3.解题思路
01背包特点:每种物品仅有一件,可以选择放或者不放。
定义f[i][v]表示前i件物品放入一个容量为v的背包可以获得的最大价值,则状态转移方程为:
f[i][v] = max {f[i-1][v],f[i-1][V-v[i]]+w[i]}
即两种策略选择最大值:
第i件物品放入时,此时的价值为:前i-1件物品放入容量为v-v[i]的背包中的价值与该物品价值w[i]的和。
第i件物品不放入时,此时的价值为:前i-1件物品放入容量为v的背包中的价值。
空间复杂度的优化
由于f[i][v]是由f[i-1][v]和f[i-1][V-v[i]]推导而来,所以只需要在循环中以v=V…0的顺序推导f[v],就能保证f[v]时,f[V-v[i]]保存的是状态f[i-1][V-c[i]]的值。
4.代码
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
* 计算01背包问题的结果
* @param V int整型 背包的体积
* @param n int整型 物品的个数
* @param vw int整型二维数组 第一维度为n,第二维度为2的二维数组,
* vw[i][0],vw[i][1]分别描述i+1个物品的vi,wi
* @return int整型
*/
public int knapsack (int V, int n, int[][] vw) {
// write code here
int[] dp = new int[V+1];
//求最大的个数,初始化值都为0
for(int i=0;i<n;i++){
int v = vw[i][0]; //第i物品的体积
int w = vw[i][1]; //第i物品的重量
for(int j=V;j>=v;j--){
if(j-v<0) continue;
dp[j] = Math.max(dp[j],dp[j-v]+w);
}
}
return dp[V];
}
5.类似题
5.1 分割等和子集
题目描述:给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
题目地址:leetcode416
解题思路:将该题转换为求解恰好装满体积为sum/2的背包的问题。当dp[sum/2]的值大于0时,证明存在一种划分可以把这两个数组分割成两个元素相等的子集。
代码:
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
for(int i=0;i<n;i++){
sum += nums[i];
}
if(sum%2!=0) return false;
sum = sum/2;
int[] dp = new int[sum+1];//装满体积为sum的背包,只要dp[sum]有值就说明分割成两个子集
for(int i=1;i<sum+1;i++){
dp[i] = Integer.MIN_VALUE; //初始化
}
for(int i=0;i<n;i++){
int num = nums[i];
for(int j=sum;j>=num;j--){
dp[j] = Math.max(dp[j],dp[j-num]+num);
}
}
return dp[sum]>0?true:false;
}
二、完全背包
1.问题描述
有一个背包,最多能容纳的体积是V。现在有n种物品,每种物品有任意多个,第i种物品的体积为vi,
价值为wi。
1)求这个背包至多能装多大价值的物品?
2)求这个背包恰好装满,至多能装多大价值的物品?
2.题目地址
3.解题思路
完全背包特点:每种物品有无限件,该物品的策略是取0件,1件,2件…,如果按照0,1背包的思想。令f[i][v]表示前i件物品恰好放入一个容量为v的背包的最大价值,则状态转移方程应该为:
f[i][v] = max {f[i-1][v],f[i-1][V-v[i]*k]+w[i]*k} 其中0<=k*v[i]<=V
此时虽然有0(N*V)个状态需要求解,但是每一个状态的求解时间是k,而不是常数。
优化
对于完全背包而言,如果两件物品i、j满足v[i]<v[j]且w[i]<w[j],则去掉物品j,不进行考虑。在随机的情况下是可以大大减少物品的件数,但是最坏情况下的复杂度还是没有改善。
考虑01背包问题空间复杂度简化时,计算f[i]时,由于要保证第i件物品只选择了一次,所以遍历顺序是逆序(从V到0),但是完全背包是每件物品可以选n次,在选择f[i]时,f[i-v[i]]的值需要是可能已经加入了该物品的价值,所以必须采用顺序遍历(0…V)
背包问题中恰好装满的最大价值和求最大价值的问题
如题中的两种问法,实现起来主要是初始化的不同。
- 如果是恰好装满背包,那么在初始化时除了f[0]为0,其他值为-∞。
- 如果只希望价格尽量大,则初始值全部设为0。
因为初始化的数组的含义是,没有任何物品可以放入背包时的合法状态。
- 如果背包要求恰好装满。那么此时只有容量为0的背包可以装满体积为0的背包,其他容量均没有合法解,所以设为-∞。
- 如果背包不是必须装满,所有物品都不装时的价值为0,所以全部值都为0.
4.代码
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param v int整型
* @param n int整型
* @param nums int整型ArrayList<ArrayList<>>
* @return int整型ArrayList
*/
public ArrayList<Integer> knapsack (int v, int n, ArrayList<ArrayList<Integer>> nums) {
// write code here
ArrayList<Integer> res = new ArrayList<>();
//求解每个背包至少能装多大价值的物品
int[] dp = new int[v+1];
int maxValue = help(dp,v,n,nums);
//求解必须装满背包的情况下的最大价值
int[] dp1 = new int[v+1];
for(int i=1;i<=v;i++){
dp1[i] = Integer.MIN_VALUE;
}
int value = help(dp1,v,n,nums);
int fullValue = value >0?value:0;
res.add(maxValue);
res.add(fullValue);
return res;
}
//完全背包问题的求解
public Integer help(int[] dp,int V,int n,ArrayList<ArrayList<Integer>> nums ){
for(int i=0;i<n;i++){
ArrayList<Integer> list = nums.get(i);
int v = list.get(0);
int w = list.get(1);
for(int j=v;j<=V;j++){
dp[j] = Math.max(dp[j],dp[j-v]+w);
}
}
return dp[V];
}
总结
解题思路参考自背包九讲,背包的变形题很多,逐渐完善。