背包问题(01、多重背包)

背包问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。常见的是 01背包和完全背包

01背包

有n种物品,每种物品只有一个。第i(i从1开始编号)件物品的重量为w\[i],价值为v\[i]。有一个给定容量为w的背包,问这个背包最多能装的最大价值是多少。   

状态:dp\[ i ]\[ j ] :从前i个物品中任选,装入容量为j的背包的最大价值
考虑计算dp\[i]\[j]的值:此时认为dp\[i]\[j]之前的值已经算出:
(1)如果第i个物品不选入背包。那么背包中只从前i-1个物品中选。那么价值为从前i-1个物品中任选,装入容量为j的背包的价值,则:dp\[ i ]\[ j ]=dp\[ i-1 ]\[ j ]

(2)如果第i个物品选入背包,那么j这么多容量中一定有一部分是i的重量w\[i],剩下的(j-w\[i])容量来放前i-1个物品中选中的物品。从前i-1个物品中任选,装入容量为(j-w\[i])的背包大价值,再加上第i个物品的价值,则:dp\[ i ]\[ j ]=dp\[ i-1 ]\[ j-w\[i]]+v\[i]。(注意此时j>=w\[i],否则数组越界)
两种情况取max。

状态转移公式:dp\[ i ]\[ j ] = max( dp\[ i - 1 ]\[ j ], dp\[ i - 1 ]\[ j - w\[ i ] ] +  v\[ i ] ) 
初始状态:一个物品都不选,dp\[0]\[j]=0;  背包容量为0,dp\[i]\[0]=0

进行降维优化:
在二维数组中,dp\[i]\[j]总是从dp\[i-1]\[…]也就是上一行推导出来, 在算第i行时除了第i-1行之外的其他行都是没有用的,所以我们可以想到,只用一个一维数组,每次计算是把i-1行复制到当前行进行计算,这样可以优化空间复杂度。每次计算都更新这一行,看起来像是在滚动一样,所以也被称作滚动数组。
dp\[j]:从前i个物品中任选,装满容量为j的背包的最大价值。
转移公式:dp\[j]=max(dp\[j],dp\[j-w\[i]]+v\[i])

需要注意的是:
每次更新的dp\[j]需要依赖其左边的部分,也就是dp\[j-w\[i]],如果从左向右遍历那么dp\[j-w\[i]]
必然已经被更新成了dp\[i]\[j-w\[i]],也就是考虑前i个物品时容量为j-w\[i]的最大值,但我们希望用到的是前一次的状态,即 每被更新时的dp\[ i - 1 ]\[ j - w\[ i ] ],因此**不能从左往右遍历**,而每次又不依赖于i右边部分的内容,所以即使右边部分被更新了,也对后续没有影响,故可以从右往左遍历。此外剩余容量j一定要比w\[i]大才能选择第i个物品,否则就不选择,也就是保持前一次的状态即可。

for(int i=1;i<=n;i++){
	for(int j=w;j>=w[i];j--){
		dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	}
}

完全背包

给你 n 种物品,每种物品都具有两个属性(价值v\[ i ]和重量w\[ i ]),每种物品都有无限多个,将他们放入容量为m 的背包中(可以重复放入同一个物品),怎么放才能让背包的价值最大?

状态不变:dp\[ i ]\[ j ] :从前i个物品中任选,装入容量为j的背包的最大价值

考虑计算dp\[i]\[j]的值,此时认为dp\[i]\[j]之前的值已经算出:
(1)如果第i个物品不选入背包。那么背包中只从前i-1个物品中选。那么价值为从前i-1个物品中任选,装入容量为j的背包的价值,则:dp\[ i ]\[ j ]=dp\[ i-1 ]\[ j ]

(2)如果第i个物品选入背包,那么j这么多容量中一定有一部分是i的重量w\[i],因为每种物品有无限个,所以当放当前的第i个物品之后仍然可以继续放该物品,因此剩下的(j-w\[i])容量可能包含之前放入的第i个物品,所以就是从前i个物品中任选,装入容量为(j-w\[i])的背包的价值,也就是从状态dp\[i]\[j-w\[i]]转移过来,则dp\[i]\[j]=dp\[ i-1 ]\[ j-w\[i]]+v\[i]

状态转移公式:dp\[ i ]\[ j ] = max( dp\[ i - 1 ]\[ j ], dp\[ i ]\[ j - w\[ i ] ] +  v\[ i ] )

进行降维优化:
01背包中的降维优化需要倒着遍历,因为每次更新当前第i行的状态需要第i-1行得到的状态,每次更新只和依赖左边部分,所以从右往左遍历不会影响左边的状态,而由完全背包状态公式可知,在在计算第i行时也只依赖第i行和第i-1行,因此也可以降维。考虑遍历方式,每次更新当前dp\[i]\[j]依赖的是已经更新过的第i行的状态dp\[i]\[j-w\[i]],只要从左往右遍历,刚好满足左边状态已经被更新成第i行的状态,所以从左往右遍历即可

for(int i=1;i<=n;i++){
	for(int j=w[i];j<=w;j++){
		dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	}
}

不同类型的题目

以上讲的都是至多型背包模型,也就是背包最多可以装w的容量,但不一定要装满,但题目有时候需要求恰好装满的情况

恰好型背包:和至多型的区别在于初始化
先考虑二维,若求最大最小值,dp\[i]\[j]表示从前i个数中选择后容量恰好为j时的最大/最小值,那么很可能会有无法满足容量恰好为j的情况,在这种情况下无法得到答案的,考虑到下一状态dp\[i+1]\[j+w]使用到状态dp\[i]\[j]时,应当也没法得到答案(前i个数无法满足容量恰好为j,那么加上当前容量w,也无法满足容量恰好为j+w),所以直接将无法恰好满足的情况赋值为一个特别小的数(负无穷/INT_MIN),这样遍历完后那些答案小于0的结果就是无法满足恰好装满的情况。

初始时让数组中所有位置为INT_MIN,但是第1列的所有值为0,表示从前i个数选择满足容量恰好为0的情况是存在的,即不选择,最大/最小值为0。此外第一行dp\[0]\[j]表示从0个数选择和恰好为j,显然只有j=0时成立。可以这样初始化:

dp[0][0] = 0;
for(int i=1;i<=n;i++){
	//这里j从0开始,j<nums[i-1],故dp[i][0]=dp[i-1][0]=0就把0列也初始化了
	for(int j=0;j<=w;j++){  
		if(j>=nums[i-1])
		//状态转移
		else{
		    dp[i][j]=dp[i-1][j]
		}
	}
}

若要是变为一维数组,每一次状态都由前一次状态推出,只需要初始化第一次状态dp\[0]=0表示和dp\[0]\[0]同样的含义,之后的每一次状态dp\[0]都不会再改变,因此也就实现了第1列全为0。

求方案数

有时候题目并非要求最大最小值,而是求方案数,此时初始化又有所不同,求方案数的状态转移为dp\[ i ]\[ j ] =dp\[ i - 1 ]\[ j ]+dp\[ i-1 ]\[ j - nums\[ i ] ],表示选择的当前nums\[i]和不选择nums\[i]的方案总和(01还是多重依据题目来定)。dp\[i]\[j]表示前i个数选择,刚好满足和/容量为j时的方案数,故从前i个数中选择使和恰好为0一定有一个方案,就是不选择,即dp\[i]\[0]=1,故二维初始化dp\[0]\[0]=1,后续遍历时j从0开始遍历即可。一维和上面同理初始化dp\[0]=1即可

问是否满足某些条件

问是否可以满足某些条件的01背包用bool类型dp数组,状态转移方程一般为dp\[ i ]\[ j ] =dp\[ i - 1 ]\[ j ] || dp\[ i ]\[ j - nums\[ i ] ],一般初始化dp\[0]\[0]=1/dp\[0]=1,思路和上面一致。
 

[和为目标值的最长子序列的长度](https://leetcode.cn/problems/length-of-the-longest-subsequence-that-sums-to-target/) 1659
[416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/)
[目标和](https://leetcode.cn/problems/target-sum/)
[将一个数字表示成幂的和的方案数](https://leetcode.cn/problems/ways-to-express-an-integer-as-sum-of-powers/) 1818  
[执行操作可获得的最大总奖励 I](https://leetcode.cn/problems/maximum-total-reward-using-operations-i/) 1849

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值