背包问题 | 豆包MarsCode AI刷题

46 阅读7分钟

0-1背包

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。物品下标从0开始。

暴力的解法是回溯搜索,不断遍历物品,根据容量来决定放哪些物品,不断重复,超时。因此需要使用动态规划来优化。

image-20241027152024096.png 思路

  1. 确定dp表

    dp[i][j]表示从下标为[0-i]的物品里任意取,放进容量为j的背包,最大价值总和,dp表的大小为n*(w+1),最终答案为dp[n-1][w],n为物品数量,w为背包最大容量。

  2. 推导状态转移方程

    每个物品只能放一次,状态转移从前一个物品来,对于是否要将物品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]

  3. 确定初始条件

    • 容量为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]

    • 其余的值初始化为任意值都行,在后续计算种都会被覆盖

  4. 确定状态转移顺序

    遍历维度为物品与背包容量,先物品后容量、先容量后物品都可以,从小到大,从大到小都可以。

    从理解角度,先物品后容量好理解,上述推导过程是从物品选取角度出发的。外循环为物品从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]了。

  5. 空间优化-滚动数组

    dp[i][j]只依赖于上一层的两个值dp[i-1][j-weight[i]]dp[i-1][j],因此用一维dp数组来滚动获取一层一层的数据。

    1. dp表

      dp[j]表示容量为j的背包的最大价值总和

    2. 状态转移方程

      简单粗暴想,二维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]
    3. 初始条件

      • 容量为0,放不了任何物品,最大价值为0,dp[0]=0

      • 除了下标为0的位置初始化为什么?根据dp[j] = max(dp[j], dp[j - weight[i]] + value[i]),只要确保后续计算的时候能够覆盖当前数值即可,题目假设物品价值都大于0,dp数组初始化为0即可。

        如果后续出现了物品价值不是正数的题就要小心了。

    4. 状态转移顺序

      • 遍历顺序:倒序容量

        根据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]],无论是按行填写还是按列填写都可以保证。按行填写就是外循环物品,内循环容量;按列填写就是外循环容量,内循环物品。

对于纯背包问题,遍历顺序如上所属,但实际应用种会在遍历顺序上出现不同。动态规划-完全背包1动态规划-完全背包2

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品。外层遍历背包容量,内层遍历物品,确保了在每个容量下都尝试所有物品,自然而然形成了顺序。

示例分析

输入:weights = [1, 2, 3]target = 4 我们来逐步计算 dp[j] 的值。

  1. 初始化dp[0] = 1(填满容量为 0 的方案只有一种:不选任何物品)
  2. 遍历容量
    • 容量 j = 1
      • 物品 1dp[1] += dp[1 - 1] = 1 所以,dp[1] = 1(方案:[1])
    • 容量 j = 2
      • 物品 1dp[2] += dp[2 - 1] = 1
      • 物品 2dp[2] += dp[2 - 2] = 1 所以,dp[2] = 2(方案:[1, 1], [2])
    • 容量 j = 3
      • 物品 1dp[3] += dp[3 - 1] = 2
      • 物品 2dp[3] += dp[3 - 2] = 1
      • 物品 3dp[3] += dp[3 - 3] = 1 所以,dp[3] = 4(方案:[1, 1, 1], [1, 2], [2, 1], [3])
    • 容量 j = 4
      • 物品 1dp[4] += dp[4 - 1] = 4
      • 物品 2dp[4] += dp[4 - 2] = 2
      • 物品 3dp[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])