动态规划-背包问题

动态规划-背包问题

动态规划中最为经典的题目必然就是背包问题, 而背包问题的基础就是01背包.

引用一个大佬梳理的背包图解

由上图可以看到,01背包其实就是背包选物品,一种物品只有一个,要么选,要么不选.

先通过一个简单的01背包问题来理解01背包问题.

01背包

有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

物品名称价值重量
物品0110
物品1215
物品2520

根据我们上一张总结的动态规划步骤,

第一步:确定dp数组

dp[i][j]:从0到i件物品中选择放入背包容量为j的背包中的最大价值

第二步:递推公式

dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
按照题目的描述,一个物品要么选,要么不选,选的话只能选一次 物品i不选的情况为dp[i-1][j],物品i选择的情况是dp[i-1][j-weight[i]] + value[i];

第三步:dp数组初始化

物品0放入容量小于它重量的背包的value应该都是0,容量为0的背包放不下任何一件物品.
dp[0][i] = 0;
dp[j][0] = 0;

第四步:确定遍历顺序

遍历的顺序:从前到后遍历

第五步:举例推导dp数组

滚动数组

前面一篇中我们有提到滚动数组,作为优化空间复杂度.

01背包是否可以用滚动数组呢?

当然是可以的,我们可以看到01背包的遍历也是一层一层的往下的

所以公式 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]).

有没有感觉有什么不对?

是的, 如果按照二维数组的遍历顺序来执行这个一位数组,会导致被重复的抽取,即从小到大的遍历顺序时,

dp[j] = max(dp[j], dp[j-weight[i]] + value[i]) 等价于二维数组的 dp[i][j] = max(dp[i-1][j], dp[i][j-weight[i]] + value[i])

所以在一维数组的公式下,我们需要以背包容量倒序的顺序进行遍历.

代码实现

func backpack01(arr [][]int, cap int) int {
	if len(arr) == 0 {
		return 0
	}
	x := len(arr)
	dp := make([][]int, x)
	for k := range dp {
		dp[k] = make([]int, cap + 1)
	}
	for i := 0; i < x; i++  {
		dp[i][0] = 0
	}
	for j := 0; j <= cap; j++ {
		if arr[0][0] <= j {
			dp[0][j] = arr[0][1]
		} else {
			dp[0][j] = 0
		}
	}
	var tmp float64
	for i := 1; i < x; i++  {
		for j := 1; j <= cap; j++ {

			if j - arr[i][0] >= 0  {
				tmp = math.Max(float64(dp[i - 1][j]), float64(dp[i - 1][j - arr[i][0]] + arr[i][1]))
			} else {
				tmp = float64(dp[i - 1][j])
			}
			dp[i][j] = int(tmp)
		}
	}
	return dp[x - 1][cap]
}

扩展

416. 分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200

示例 1:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

输入: [1, 2, 3, 5]

输出: false

解释: 数组不能分割成两个元素和相等的子集.

完全背包

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。

物品名称价值重量
物品0110
物品1215
物品2520

根据动态规划步骤,

第一步:确定dp数组

dp[i][j]:从0到i件物品中选择放入背包容量为j的背包中的最大价值,这个和01背包一样

第二步:递推公式

dp[i][j] = max(dp[i-1][j], dp[i][j-weight[i]] + value[i])
按照题目的描述,一个物品要么选,要么不选,选的话可以选无数次 物品i不选的情况为dp[i-1][j],物品i选择的情况是dp[i][j-weight[i]] + value[i];也就是即使选过了第i个物品,也还能继续选i物品

第三步:dp数组初始化

物品0放入容量小于它重量的背包的value应该都是0,由于一个物品可以多次放置,所以完全背包初始化的时候与01背包有些许的不通,容量为0的背包放不下任何一件物品.
dp[0][i] = 0;
dp[j][0] = 0;

第四步:确定遍历顺序

遍历的顺序:从前到后遍历

第五步:举例推导dp数组

func backpack(arr [][]int, cap int) int {
	if len(arr) == 0 {
		return 0
	}
	x := len(arr)
	dp := make([][]int, x)
	for k := range dp {
		dp[k] = make([]int, cap + 1)
	}
	for i := 0; i < x; i++  {
		dp[i][0] = 0
	}
	for j := 0; j <= cap; j++ {
		if (j / arr[0][0]) > 0  {
			dp[0][j] = arr[0][1] * (j / arr[0][0])
		} else {
			dp[0][j] = 0
		}
	}
	var tmp float64
	for i := 1; i < x; i++  {
		for j := 1; j <= cap; j++ {

			if j - arr[i][0] >= 0  {
				tmp = math.Max(float64(dp[i - 1][j]), float64(dp[i][j - arr[i][0]] + arr[i][1]))
			} else {
				tmp = float64(dp[i - 1][j])
			}
			dp[i][j] = int(tmp)
		}
	}
	return dp[x - 1][cap]
}

滚动数组

完全背包是否可以用滚动数组来做空间复杂度的优化呢.从我们上面推导的二维数组的递推公式再结合01背包中我们推导一维数组的思路,可以看到完全背包也是可以做滚动数组的.只是在遍历顺序上,背包容量的遍历顺序与01背包的遍历顺序恰好相反,是从小到大的遍历.

func backpackTwo(arr [][]int, cap int) int {
	if len(arr) == 0 {
		return 0
	}
	x := len(arr)
	dp := make([]int, cap + 1)
	var tmp float64
	for i := 0; i < x; i++  {
		for j := 0; j <= cap; j++ {
			if j - arr[i][0] >= 0  {
				tmp = math.Max(float64(dp[j]), float64(dp[j - arr[i][0]] + arr[i][1]))
			} else {
				tmp = float64(dp[j])
			}
			dp[j] = int(tmp)
		}
	}
	return dp[cap]
}

扩展

518. 零钱兑换 II

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

输入: amount = 5, coins = [1, 2, 5]

输出: 4

解释: 有四种方式可以凑成总金额:

5=5

5=2+2+1

5=2+1+1+1

5=1+1+1+1+1

示例 2:

输入: amount = 3, coins = [2]

输出: 0

解释: 只用面额2的硬币不能凑成总金额3。

学习渠道: 微信公众号 代码随想录

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值