0-1背包
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。物品下标从0开始。
暴力的解法是回溯搜索,不断遍历物品,根据容量来决定放哪些物品,不断重复,超时。因此需要使用动态规划来优化。
思路
-
确定dp表
dp[i][j]表示从下标为[0-i]的物品里任意取,放进容量为j的背包,最大价值总和,dp表的大小为n*(w+1),最终答案为dp[n-1][w],n为物品数量,w为背包最大容量。 -
推导状态转移方程
每个物品只能放一次,状态转移从前一个物品来,对于是否要将物品i放入背包的考量:
- 放入物品i,背包要留足物品i的容量,此时价值为
dp[i-1][j-weight[i]]+value[i] - 不放入物品i,背包情况和考虑放入
[0,i-1]情况相同,此时价值为dp[i-1][j]
价值总和要取最大,结合上述两种情况,
-
当
j>=weight[i]时,有足够容量考虑是否放入物品i,dp[i][j]=max( dp[i-1][j-weight[i]]+value[i] , dp[i-1][j] ) -
当
j<weight[i],没有足够的容量考虑是否放入物品i,dp[i][j]=dp[i-1][j]
- 放入物品i,背包要留足物品i的容量,此时价值为
-
确定初始条件
-
容量为0,装不了任何物品,最大价值总和为0,
dp[i][0]=0 -
根据状态转移方程,
dp[i]依赖于dp[i-1],数组不越界i-1>=0,故需要初始化dp[0],需要先填写考虑物品0的最大价值总和。当
j<weight[0]时,无法装入物品0,dp[0][j]=0当
j>=weight[0]时,可以装入物品0,dp[0][j]=value[0] -
其余的值初始化为任意值都行,在后续计算种都会被覆盖
-
-
确定状态转移顺序
遍历维度为物品与背包容量,先物品后容量、先容量后物品都可以,从小到大,从大到小都可以。
从理解角度,先物品后容量好理解,上述推导过程是从物品选取角度出发的。外循环为物品从0到n,内循环为容量从0到w,相当一行一行填写dp表
从状态转移方程角度想,
dp[i][j]=max( dp[i-1][j-weight[i]]+value[i] , dp[i-1][j] ),dp[i][j]依赖于上一层的两个值dp[i-1][j-weight[i]]和dp[i-1][j],这两个值在(i,j)上方和左上方。按照外循环容量从0到w,内循环从0到n,相当于按列填写dp表,填写dp[i][j]时已经计算出来dp[i-1][j-weight[i]]和dp[i-1][j]了。 -
空间优化-滚动数组
dp[i][j]只依赖于上一层的两个值dp[i-1][j-weight[i]]和dp[i-1][j],因此用一维dp数组来滚动获取一层一层的数据。-
dp表
dp[j]表示容量为j的背包的最大价值总和 -
状态转移方程
简单粗暴想,二维dp数组的递推公式为
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]),只要坍缩掉物品维度即可,dp[j] = max(dp[j], dp[j - weight[i]] + value[i])。观察上式,根据容量来考量,分为
j>=weight[i]和j<weight[i]- 当
j>=weight[i]时,有足够容量考虑是否放入物品i,dp[j]=max( dp[j-weight[i]]+value[i] , dp[j] ) - 当
j<weight[i],没有足够的容量考虑是否放入物品i,维持考虑物品i-1的情况,dp[j]=dp[j]
- 当
-
初始条件
-
容量为0,放不了任何物品,最大价值为0,
dp[0]=0 -
除了下标为0的位置初始化为什么?根据
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]),只要确保后续计算的时候能够覆盖当前数值即可,题目假设物品价值都大于0,dp数组初始化为0即可。如果后续出现了物品价值不是正数的题就要小心了。
-
-
状态转移顺序
-
遍历顺序:倒序容量
根据
dp[j]=max( dp[j-weight[i]]+value[i] , dp[j] ),计算过程依赖先前一层的计算结果dp[j-weight[i]]+value[i]和dp[j],为了在计算dp[j]时能够使用先前的dp[j-weight[i]],对于容量的遍历应该从w到0,对物品的遍历应该从0到n。 -
内外循环:外循环物品,内循环容量
咱就是说按照思考过程来写不就好了。。。
为什么倒过来不行?
容量一定是倒序遍历,如果遍历背包容量放在外层,遍历物品放在内层,
dp[j]=max( dp[j-weight[i]]+value[i] , dp[j] ),每次从后往前填写,计算所依赖的dp[j-weight[i]]没有被计算出来,全都是0,变成了value[i]的比较,仅仅是根据特定容量去考虑放一个物品能取得的最大价值。。。我们选择坍缩掉i维度,但是推导过程都是按照选取i来思考的,遍历背包容量放在外层,遍历物品放在内层,相当于按列来填写dp表,必然错误。
-
-
代码
func kbapsackDP(weigt, value []int, cap int) int {
n:=len(weight)
// 创建dp表
dp := make([][]int, n)
for i := 0; i < n; i++ {
dp[i] = make([]int, cap+1)
}
//初始化 dp[i][0]=0
//当`j<weight[0]`时,无法装入物品0,`dp[0][j]=0`
//当`j>=weight[0]`时,可以装入物品0,`dp[0][j]=value[0]`
for j:=weight[0];j<=weight;j++{
dp[0][j]=value[0]
}
//状态转移
for i:=1;i<n;i++ {//遍历物品
for j:=0;j<=cap;j++ {//遍历背包容量,j=0的话dp都是0吧,在规避物品容量为0
if j< weight[i] {//无法装入物品i
dp[i][j]=dp[i-1][j]
}else {//可以考虑装入物品i
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+ value[i])
}
}
}
return dp[n-1][cap]
}
空间优化
func kbapsackDP(weigt, value []int, cap int) int {
n:=len(weight)
// 创建dp表
dp := make([]int, cap+1)
//初始化dp[j]=0
//状态转移
for i := 0; i < n; i++ {//遍历物品
for j := cap; j >= weight[i]; j-- {//遍历背包,容量过小时维持上一层结果,直接跳过
//容量足够放下物品i,要考虑是否放入
dp[j] = max(dp[j], dp[j-weight[i]] + value[i])
}
}
return dp[cap]
}
完全背包
完全背包的物品可以被多次添加,因此只要在0-1背包的基础上改变遍历顺序,在遍历容量时从前往后即可。
for(i := 0; i < weight.size(); i++) { // 遍历物品
for( j := weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
遍历顺序上,两个循环的嵌套顺序是无所谓的,因为dp[j]依赖于先前计算过的本层的结果dp[j-weight[j]],无论是按行填写还是按列填写都可以保证。按行填写就是外循环物品,内循环容量;按列填写就是外循环容量,内循环物品。
对于纯背包问题,遍历顺序如上所属,但实际应用种会在遍历顺序上出现不同。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。外层遍历背包容量,内层遍历物品,确保了在每个容量下都尝试所有物品,自然而然形成了顺序。
示例分析
输入:weights = [1, 2, 3],target = 4
我们来逐步计算 dp[j] 的值。
- 初始化:
dp[0] = 1(填满容量为 0 的方案只有一种:不选任何物品) - 遍历容量:
- 容量 j = 1:
- 物品
1:dp[1] += dp[1 - 1] = 1所以,dp[1] = 1(方案:[1])
- 物品
- 容量 j = 2:
- 物品
1:dp[2] += dp[2 - 1] = 1 - 物品
2:dp[2] += dp[2 - 2] = 1所以,dp[2] = 2(方案:[1, 1], [2])
- 物品
- 容量 j = 3:
- 物品
1:dp[3] += dp[3 - 1] = 2 - 物品
2:dp[3] += dp[3 - 2] = 1 - 物品
3:dp[3] += dp[3 - 3] = 1所以,dp[3] = 4(方案:[1, 1, 1], [1, 2], [2, 1], [3])
- 物品
- 容量 j = 4:
- 物品
1:dp[4] += dp[4 - 1] = 4 - 物品
2:dp[4] += dp[4 - 2] = 2 - 物品
3:dp[4] += dp[4 - 3] = 1所以,dp[4] = 7(方案:[1, 1, 1, 1], [1, 1, 2], [1, 2, 1], [2, 1, 1], [2, 2], [1, 3], [3, 1])
- 物品
- 容量 j = 1: