理解背包问题

目录

01背包问题

解法一:暴力解法

解法二:动态规划

二维dp

一、dp数组的含义

二、dp递推公式

三、dp数组初始化

四、dp数组遍历顺序

五、打印dp数组

一维dp

一、dp数组的含义

二、dp递推公式

三、dp数组初始化

四、dp数组遍历顺序


图片来源:动态规划:01背包理论基础

01背包问题

        

物品重量价值
0115
1320
2430

背包最大重量为4,问背包能背的物品最大价值是多少?

这是一个典型的01背包问题,其他很多题目其实都需要转化为01背包问题,所以我们先来搞明白01背包问题的解法

解法一:暴力解法

其实这个问题对于物品来说就是选于不选的问题,那么我们可以用回溯法,来遍历所有的可能情况,这时时间复杂度就是O(2^n),n表示物品数量。

解法二:动态规划

上述暴力解法的时间复杂度是指数增长的,所以需要用动态规划来优化时间复杂度。下面,将按照代码随想录当中介绍的动态规划五部曲来分解01背包问题。

二维dp

一、dp数组的含义

对于二维dp数组dp[i][j]的含义为:任取小标为0-i的物品放进容量为j的背包,价值总和最大为多少。

二、dp递推公式

dp[i][j]可以由两个部分推导出

  • 对于物品i,我们选择不取,那么dp[i][j] = dp[i-1][j],即因为背包容量为j,里面不放物品i的最大价值(也就是物品i的重量大于背包j的重量时),背包里的价值依然和前面相同
  • 放物品i,那么由dp[i-1][j-weight[j]]推导出,此时的含义为背包容量为j-weight[i]时,不放物品i的最大价值,那么dp[i-1][j-weight[i]]+value[i]就表示放了物品i的价值

所以递推公式为:dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])

三、dp数组初始化

由递推公式我们可知,i由[i-1][j][i-1][j-weight[i]]推导出,那么我们需要将dp数组的第一列和第一行都进行初始化。

  • 第一列初始化都可以为0,因为此时背包容量为0无法放进任何物品
  • 第一行的初始化需要在j >= weight[0]的位置开始,将dp[0][j] = value[i]
01234
物品0015151515
物品10
物品20
四、dp数组遍历顺序

对于二维dp,数组的遍历的顺序和最后的结果没有太大的关系,因为dp[i][j]的值有其上方和左上方推导而来,只要这两个方向有值,那么不管是哪种遍历顺序,其实都能够得到相对于的结果。

dp数组遍历图
01234
物品0015151515
物品1015152035
物品2015152035
五、打印dp数组

这部分是在程序调试的过程中来实现,下面是基于Go语言的实现

func main() {
    // 如果是参数版,那么需要传递的参数为:(weight, value []int, bagweight)
	weight := []int{1, 3, 4}
	value := []int{15, 20, 30}
	bagweight := 4
	dp := make([][]int, len(weight))
	for i := 0; i < len(weight); i++ {
		dp[i] = make([]int, bagweight+1)
	}
    // dp数组初始化
	for i := weight[0]; i < bagweight+1; i++ {
		dp[0][i] = value[0]
	}
	for i := 1; i < len(weight); i++ { // 遍历物品
		for j := 1; j < bagweight+1; j++ { // 遍历背包

			if j < weight[i] {
				dp[i][j] = dp[i-1][j]
			} else {
				dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
			}
		}
	}
	fmt.Println(dp[len(weight)-1][bagweight])
}
func max(a, b int) int {
	if a > b {
		return a
	} else {
		return b
	}
}

一维dp(滚动数组)

还是基于上述例子来理解。

对于背包问题其实状态是可以压缩的,在使用二维数组时,递推公式:dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i]),其实这里第i层可以直接复用第i-1层的信息,那么递推公式就变成dp[i][j] = max(dp[i][j], dp[i][j-weight[i]]+value[i]),这样就相当于是前i-1个物品已经放进了当前背包容量j中,那么当前物品i放入背包容量j中是否能使得价值总和最大。

因此经过上述分析,我们不难发现,与其使用二维数组增大时间和空间开销,不如使用一维数组来实现上述描述。此时dp数组的含义如下所示:

一、dp数组的含义

dp[j]:表示当前容量j的背包中,所背物品的价值最大可为dp[j]

二、dp递推公式

由于这里是一维数组,那么dp[j]只能由一个公式推导而来:dp[j-weight[i]]+value[i]

这个式子表示在容量为j-weight[i]的背包(该容量下已达最大价值)中加上物品i的价值,就表示此时容量为j的背包选择了物品i时的背包内价值是多少。这里其实也涉及到物品i的选择与不选择问题,上述式子表示选择了物品i,而不选择物品i时,dp[j]的值维持不变。这里为什么是维持不变,在下面讲述dp数组的遍历顺序时就能够明白。

  • 完整的递推公式:dp[j] = max(dp[j], dp[j-weight[i]]+value[i])
三、dp数组初始化

dp[j]表示容量为j的背包,所背的物品的价值可以最大为单dp[j],那么dp[0] = 0,其他非0下标的初始化则看给的物品价值是否为正值,因为递推公式的含义为物品i选与不选的最大值,在最初始阶段没有物品来,只要不影响后续物品的选则,那么我们给dp数组非0下标的初始化为0就可以了。

四、dp数组遍历顺序

一维dp的遍历顺序和二维dp的遍历顺序区别很大。

在二维dp数组的遍历中,我们说优先遍历物品还是优先遍历背包其实都没有太大的区别,只要保证遍历时上方和左上方有值存在就可以。

但是在一维数组遍历中,这个顺序就不能随意。这里直接给出结论:我们遍历是先遍历物品,再遍历背包,切背包遍历应该倒序遍历

我们从下面几个方面来分析为什么:

1、如果我们在先遍历物品,再遍历背包的时候,选择正序遍历

使用正序遍历,会导致同一个物品被多次使用,在01背包问题中,每个物品只能使用一次

我们使用图片样例来展示这个错误:

dp数组初始化:

遍历到物品0,当遍历背包时,从容量为1开始,发现可以放下,则dp[1] = 15:

遍历到物品0,背包遍历到容量2,此时依然可以放下物品0,那么这里dp[2] = dp[2-weight[0]]+value[0] = dp[1] + 15 = 15 + 15 =30,这时意味着物品0被放入了两次。

因此,我们需要倒序遍历,下面给出倒叙遍历的图示样例:

1)物品0

2)物品1

3)物品2

在这里会对dp[4] = 35dp[4-4] + value[2] = 0 + 30 = 30进行对比,选择大的那个

2、如果我们先遍历背包,再遍历物品

解释完为什么要倒叙遍历,接下来解释为什么不可以先遍历背包再遍历物品

如果我们先遍历背包那就是在当前容量j下,物品i是否能够放进去。其实这样无法保存背包之前存入物品的信息,后续的物品会覆盖之前存入背包的物品,相当于只放入了一个物品。为什么会这么说呢?我们看递推公式就能够明白,当物品i已经放入背包j时,下一个物品来临,max操作会在dp[j]dp[j-weight[i]]+value[i]中选择一个,而这个时候背包j中只有物品i,前置背包中还没有物品,那相当于就是物品i和物品i+1比较谁的价值,谁就放入背包j中。

我们以一个图例来直观的展示这个错误:

1) 容量为4的背包

物品0

物品1,此时还是在容量为4的背包下,那么按照递推公式来推导:dp[4] = max(dp[4], dp[4-weight[1]+value[1]] = max(15, dp[1] + 20) = max(15, 0 + 20) = 20。到这里可以很直观的看到,本来容量为4的背包可以放入物品0和物品1两个,但是这样遍历物品1直接覆盖了物品0,无法得到最大的所背物品价值。

五、打印dp数组

这部分是在程序调试的过程中来实现,下面是基于Go语言的实现

func main() {
	// 如果是参数版,那么需要传递的参数为:(weight, value []int, bagweight)
	weight := []int{1, 3, 4}
	value := []int{15, 20, 30}
	bagweight := 4
	dp := make([]int, bagweight+1) // 初始化为0

	for i := 0; i < len(weight); i++ { // 遍历物品
		for j := bagweight; j >= weight[i]; j-- { // 倒序遍历背包

			dp[j] = max1(dp[j], dp[j-weight[i]]+value[i])
		}
		fmt.Println(dp)
	}
	fmt.Println(dp[bagweight])
}
func max1(a, b int) int {
	if a > b {
		return a
	} else {
		return b
	}
}

结果:

完全背包问题

待续.....

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值