背包问题
- 常见的背包问题类型(大厂面试重点掌握01背包和完全背包即可)
- 题目描述:有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品能用*次,求解怎么装物品使得装入背包里物品价值总和最大。
01背包(二维数组解法和滚动数组解法)
https://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html
视频讲解:https://www.bilibili.com/video/BV1cg411g7Y6
- 题目描述:有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
- 二维数组解法
- 动规五部曲
- 二维dp数组dp[i][j]
- 其中i代表有前 i 个物品可选,j代表背包的容量,dp值代表对应的最大价值
- 递推公式
- 当前 i 和 j 对应的最大价值为
装第i个物品
和不装第i个物品
两种情况里的较大值 - 其中装第i个物品对应的dp值为
dp[i - 1][j - weight[i]] + value[i]
,weight[i]表示第i个物品的重量,value[i]代表第i个物品的价值 - 不装第i个物品对应的dp值为dp[i - 1][j]
- 即
dp[i][j] = max(dp[i - 1][j - weight[i]], dp[i - 1][j] + value[i])
- 当前 i 和 j 对应的最大价值为
- 二维dp数组的初始化
- 当把二维dp数组画图表示出来后,可以发现dp[i][j]的值仅由其上方或左上方的元素值决定,因此需要把dp数组的第一行和第一列进行初始化
- 第一行的初始化(第一行对应只有物品1和背包容量递增的情况):由于是01背包,所以在背包容量 < 物品1重量时,初始化为0,当背包容量 >= 物品1重量时,均初始化为物品1的价值
- 第一列的初始化(即物品由少到多但背包容量为0的情况):全部初始化为0
- 遍历顺序
- 内外层for循环是先遍历物品还是背包容量都可以,因为二维数组图里无论哪种遍历方式都满足当前元素的上方和左上方元素已有值
- 每层for循环从小到大遍历
- 无需打印
- 二维dp数组dp[i][j]
- 代码书写问题
- 问题1,如果直接把背包容量n作为dp数组一维的大小,则需要在初始化dp数组的第一列时(此时第一列为背包容量为1的情况)就要使用递推公式,因此将dp的一维初始化为(n+1)的大小,这样背包容量从0开始,初始化不需要递推公式
- 问题2,最后直接打印或返回dp数组的最后一个元素即可,因为dp数组第[i][j]个元素的含义就是有i个物品可选且背包容量为j的时候最大价值是多少
- 问题3,写递推公式的时候,由于当前背包的容量可能小于当前物品的重量,因此应添加额外判断,当小于时,只剩下不取当前物品的情况了
- 问题4,在初始化dp数组第一行的时候,可以直接从weight[0]开始,因为前面的位置已经在创建时赋0了,不需要进行修改
- 动规五部曲
# 二维数组解法
# m种物品,背包容量为n,每个物品的重量和价值分别保存在weight和value里
dp = [[0] * (n + 1) for _ in range(m)]
for j in range(weight[0], n + 1):dp[0][j] = value[0]
for i in range(1, m):for j in range(1, n + 1):if (j - weight[i]) >= 0:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])else:dp[i][j] = dp[i - 1][j]
print(dp[-1][-1])
- 滚动数组(一维数组解法)
- 在二维数组解法的基础上,我们可以发现每次更新dp数组元素时,都是依赖且仅依赖于第 i - 1行元素的值,因此,如果我们仅使用一维数组,而在循环过程中滚动更新该数组,应该可以实现和二维数组相同的效果
- 为什么要这样做?使用一维数组代替二维数组可以显著地优化代码的时间复杂度和空间复杂度
- 如果一下子觉得一维数组写法比较抽象,可以先写二维数组,之后微调就是一维数组解法
- 动规五部曲
- 一维dp数组dp[j]
- 其中j代表背包的容量,dp值代表对应的最大价值
- 递推公式
- 当前 j 对应的最大价值为
装第i个物品
和不装第i个物品
两种情况里的较大值 - 其中装第i个物品对应的dp值为
dp[j - weight[i]] + value[i]
,weight[i]表示第i个物品的重量,value[i]代表第i个物品的价值 - 不装第i个物品对应的dp值为dp[j]
- 即
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
- 当前 j 对应的最大价值为
- 一维dp数组的初始化
- (对应只有第一个物品和背包容量递增的情况):由于是01背包,所以在背包容量 < 物品1重量时,初始化为0,当背包容量 >= 物品1重量时,均初始化为物品1的价值
- 遍历顺序
- 内外层for循环是先遍历物品、再遍历背包容量,因为相当于把二维数组压扁了,此时后遍历物品没有意义,将得到一个全部为最高价值物品价值的一维数组
- 外层for循环从第一个物品开始遍历(顺序遍历),内层for循环(背包容量)从大到小遍历(逆序遍历),因为要保证每个元素所依赖的前置位元素为二维数组里的左上元素而不是同一行的左边元素
- 无需打印
- 一维dp数组dp[j]
- 代码书写问题
- 内层for循环逆序遍历时,要注意其实索引是n而不是n+1
# 一维数组解法
# m种物品,背包容量为n,每个物品的重量和价值分别保存在weight和value里
dp = [0] * (n + 1)
for j in range(weight[0], n + 1):dp[j] = value[0]
for i in range(1, m):for j in range(n, 0, -1):if (j - weight[i]) >= 0:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
print(dp[-1])
*416. 分割等和子集
https://programmercarl.com/0416.%E5%88%86%E5%89%B2%E7%AD%89%E5%92%8C%E5%AD%90%E9%9B%86.html
视频讲解:https://www.bilibili.com/video/BV1rt4y1N7jE
- 考点
- 动态规划
- 01背包
- 我的思路
- 本题可以用回溯暴力求解,但大概率超时
- 视频讲解关键点总结
- 关键点1,分割等和子集等价转化为从集合里取元素,使其和值填满集合和值的一半(则剩下元素和当前取的元素构成了等和子集)
- 关键点2,由于每个元素只能取一次,并且相当于从前 i 个物品里取物品来填满容量为集合值一半的背包,所以本题可以抽象为01背包问题
- 动规五部曲
- 一维数组dp[j]的含义是,当背包容量为 j 时,取前 i 个元素(i通过外层循环来取,替代原有的二维数组),能得到的不超过背包容量的最大和值(可以等于)
- 递推公式
- dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])(取当前元素和不取当前元素的较大者)
- 初始化,dp初始化为0即可,因为背包容量为0时最大值就为0
- 遍历顺序
- 外层循环遍历元素 i ,内存循环遍历容量 j
- 外层循环正向遍历,内层循环反向遍历(避免将同一元素计算两次)
- 无需打印
- 我的思路的问题
- 无思路
- 代码书写问题
- 注意内层循环的停止位置
- 可执行代码
class Solution:def canPartition(self, nums: List[int]) -> bool:summation = sum(nums)if summation % 2 == 1:return Falsesummation /= 2summation = int(summation)dp = [0] * (summation + 1)for i in range(len(nums)):for j in range(summation, nums[i] - 1, -1):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])if dp[-1] == int(summation):return Truereturn False