背包问题(0-1背包+完全背包)
Posted lyeeer
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了背包问题(0-1背包+完全背包)相关的知识,希望对你有一定的参考价值。
0-1背包
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
重要的点在于:每种物品仅有一件,可以选择放/不放
子问题:f[i][v]表示前i件物品恰好放入一个 容量为v 的背包可以获得的最大价值。
状态转移方程(递推式):f[i][v]=max{f[i-1][v], f[i-1][v-c[i]]+w[i]};//考虑前i件物品放入这个子问题的时候,可以转化为前i-1件物品已经放好。那么如果放入第i件物品,那么问题转化为 前i-1件物品放入剩余容量为v-c[i]的背包里;如果不放入第i件物品,那么问题转化为 前i-1件物品放入剩余容量为v的背包里。
而如果放入第i件物品,那么当前价值就是f[i-1][v-c[i]]+w[i]。因此当前最大价值就是 放入&不放入 之间的最大值。
可以反向找到各种物品的选择:从dp[N][V]开始,如果dp[i][j]=dp[i-1][j],则当前第i件物品没有被选中,从dp[i-1][j]继续找;否则,则表示选中,从dp[i-1][j-w[i]]开始找
伪代码:
int[][] dp=new int[N][V+1]; //初始化第一行 //仅考虑容量为V的背包放第0个物品,不放物品,价值为0 for(int i=0;i<=V;i++){ dp[0][i]=0; } //初始化第一列
//容量为0的背包放物品,不放物品,价值为0
for(int i=0;i<=N;i++){
dp[i][0]=0;
} //根据状态转移方程,填充其他行和列 for(int i=1;i<N;i++){ for(int j=1;j<=V;j++){
//装不进去,当前剩余空间小于第i个物品的大小
if(w[i]>j){
dp[i][j]=dp[i-1][j];
}
//容量足够,可以放进去,比价值更大的方法
else{ dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]); } } } //最后的结果 dp[N-1][V]
如何优化呢?那么只能在空间复杂度上进行优化,只使用一维数组来存放结果
此时状态转移方程为:f[v]=max{f[v], f[v-c[i]]};//那么这个时候需要注意的是,在第二层循环中,需要使用从后往前计算,得到结果。即要用一维数组记忆化的时候,需要用到当前位置的值 和 该位置之前的值。因为如果我们需要计算f[4, 4]=max{f[3,4], f[3,1]}、f[4,3]=max{f[3,3], f[3,1]}、f[4,2]=max{f[3,2], f[3,1]}。
如果是转化为一维数组,因为需要保证max中的f[v]是f[i-1][v],前面的f[v]是f[i][v]。也就是当前这一层的d[j]还没有被更新过,所以当前的d[j]用到的是i-1层的结果。如果从前往后计算,那么下一次使用的d[j]是本层已经更新过的,会覆盖掉i-1层的结果。
//解释
由于知道dp[i-1,1...j]就可以得到dp[i,j],下一层只需要根据上一层结果就可以推出答案。
对于dp[j]=max{dp[j], dp[j-w[i]]+v[i]}而言,dp[j-w[i]]相当于二维的dp[i-1][j-w[i]],dp[j]是由前面的dp(1...j)推出来的。
因此比如从i=3推i=4,此时一维数组存放{0,0,2,4,4,6,6,6,7,7,9},这是i=3时所有子问题的解。如果从前往后推,那么计算i=4时,
dp[0]=0, dp[1]=0, ... , (前面这几项都放不进 w[i]=5的物品)dp[5]=max{dp[5], dp[5-5]+7}=7, dp[6]=max{dp[6], dp[6-5]+7}=7, dp[7]=max{dp[7], dp[7-5]+7}=9.....这里会更新dp[5]、dp[6]...的值,那么后续计算的时候 就没办法用到 上一轮循环时的 dp[5]、dp[6]....了(即 因为当前值 是由上一轮循环推出来的,如果从前往后,前一次循环保存下来的值 可能会被修改)就是我当前更新要用到这个值,但是这个值 在从前往后更新时,已经被修改了,那么我用到的就是错误的值了。
初始化的话:(初始化 实际上是 在没有任何物品可以放入背包时 的合法状态)
如果问法是“恰好装满”的最优解,那么除了dp[0]初始化为0,其他都应该设置为 负无穷大。这样能保证最终的dp[V]为恰好装满背包时的最优解。此时,只有容量为0的背包 可以在 什么都不装且价值为0时被“恰好装满”,因为如dp[3]则表示,背包容量为3时,恰好装满的价值,此时没有合法的解,因此属于未定义状态,设为无穷大。
如果问法是“可以不装满”的最优解,那么所有的都应初始化为0,因为“什么都不装”时,0就是合法解。
伪代码:
int[] dp=new int[V+1]; //初始化第一行 //仅考虑容量为V的背包放第0个物品,不放物品,价值为0 for(int i=0;i<=V;i++){ dp[i]=w[0]<=i?v[0]:0; } //根据状态转移方程,填充其他行和列 for(int i=1;i<N;i++){ for(int j=V;j>=w[i];j--){ dp[j]=Math.max(dp[j],dp[j-w[i]]+v[i]); } } //最后的结果 dp[V]
例题:给定一个仅包含正整数的非空数组,确定该数组是否可以分成两部分,要求两部分的和相等
思路:即给定N个元素组成的数组arr,数组元素的和为sum。转换成背包问题,每个物品的重量和价值为arr[i],两部分和相等,即背包的限重为sum/2.
if(nums==null || nums.length==0){ return true; } int sum=0; for(int num : nums){ sum+=num; } //如果sum不可以平分,那么就不可分为两块 if(sum%2!=0){ return false; } sum/=2; //定义 boolean[] dp=new boolean[sum+1]; //初始化
dp[0]=true; for(int i=1; i<=nums.length; i++){
//为什么要从后往前更新dp,因为每个位置 依赖于 前面一个位置 加上 nums[i]。如果从前往后更新的话,
那么dp[i-2]会影响dp[i-1],然后dp[i-1]会影响dp[i],即同样的一个nums[i]被反复使用了多次。
for(int j=sum; j>=nums[i]; j--){ dp[j]=dp[j] || dp[j-nums[i]]; } } //输出 dp[sum]
完全背包
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
重要的区别在于完全背包是 每种无限件
状态转移方程:f[i][j]=Math.max(f[i-1][j-k*c[i]]+k*w[i]),0<=k*c[i]<=j;//根据第i件物品放多少件,即前i-1件物品中 选择 若干件 放入剩余的空间上,使得最大。(f[i][j]表示 前i种物品 放入一个容量为j的背包种获得 最大价值)
//递归和动态规划的区别:动态规划多使用了一个二维数组来存储中间的解
代码:
int[][] dp=new int[N][V+1]; //初始化第一行 //仅考虑容量为V的背包放第0个物品,不放物品,价值为0 for(int i=0;i<=V;i++){ dp[0][i]=0; } //初始化第一列 //容量为0的背包放物品,不放物品,价值为0 for(int i=0;i<=N;i++){ dp[i][0]=0; } //根据状态转移方程,填充其他行和列 for(int i=1;i<N;i++){ for(int j=1;j<=V;j++){ //装不进去,当前剩余空间小于第i个物品的大小 if(w[i]>j){ dp[i][j]=dp[i-1][j]; } //容量足够,可以放进去,比价值更大的方法。取k个物品i,再k种选择 选出 最优解 else{ for(int k=0; k*w[i]<=j; k++){ dp[i][j]=Math.max(dp[i-1][j], dp[i-1][j-w[i]*k])+v[i]*k; } } } } //最后的结果 dp[N-1][V]
同样使用一维数组 来优化 空间复杂度
dp[i]=Math.max(dp[i], dp[i-w[i]]+v[i])
int[] dp=new int[V+1]; //初始化第一行 //仅考虑容量为V的背包放第0个物品,不放物品,价值为0 for(int i=0;i<=V;i++){ dp[i]=0; } //根据状态转移方程,填充其他行和列 for(int i=1;i<N;i++){ for(int j=w[i];j<=V;j++){ dp[j]=Math.max(dp[j],dp[j-w[i]]+v[i]); } } //最后的结果 dp[V]
以上是关于背包问题(0-1背包+完全背包)的主要内容,如果未能解决你的问题,请参考以下文章