LeetCode 背包问题¶
本文是我学习背包问题的一些总结。
算法题算是我求职路上的一道坎,我没有数据结构与算法的学习经验,但许多笔试都需要考算法题。我花了两天时间系统学习动态规划中的背包问题,希望能让自己再次遇到这类问题时不再畏惧。之前以为只有计算机系同学才能做出的题目,我也能自己做出来了,这就是学习的收获。
背包问题是指有一个固定容量为c
的背包,以及一组物品,每个物品都有自己的重量w[i]
和价值p[i]
。在不超过背包容量的前提下,选择不同的物品装入背包,使得装入背包中的物品总价值最大。
01 背包¶
在 01 背包问题中,每个物品只能用一次。
可以用 二维数组 或 一维数组 来解答,后者的空间复杂度更低,因此我们后续都使用一维数组。
总结
使用一维数组求解 01 背包问题时,必须先遍历物品,再遍历背包容量,并且遍历背包容量时需要 倒序 。
为什么要倒序?一个比较好的解释是(来源:【带你学透 01 背包问题(滚动数组篇) | 从此对背包问题不再迷茫!】 ):
问题角度 1:给定背包容量,最多能装多少价值的物品?¶
这是最基础的问题角度,步骤如下:
- 确定
dp
数组的定义。
在一维dp
数组中,dp[j]
表示:容量为j
的背包,所背的物品价值可以最大为dp[j]
。
- 初始化
dp
数组。它的 长度为c+1
,初始值都为0
。
- 遍历所有物品,倒序遍历背包容量(从
dp[c]
遍历到dp[w[i]]
,因为小于w[i]
的背包容量是不会发生变化的)。
# 遍历物品
for i in range(len(w)):
# 倒序遍历背包容量
for j in range(c, w[i] - 1, -1):
dp[j] = max(dp[j], dp[j - w[i]] + p[i])
由此角度衍生出的问题¶
- 416. 分割等和子集 是求给定背包容量,能不能装满这个背包。
- 1049. 最后一块石头的重量 II 是求给定背包容量,尽可能装,最多能装多少。
- 0474. 一和零 是求给定背包容量,装满背包最多有多少个物品。注意背包是二维的,可以理解为:背包最多能装
m
个 0 个n
个 1。再将每个物品的价值抽象为 1,我们需要最大化背包里面的物品个数,也就是最大化背包的价值。
问题角度 2:给定背包容量,装满背包有多少种方法?¶
- 确定
dp
数组以及下标的含义。
dp[j]
表示:填满j
(包括j
)这么大容积的背包,有dp[j]
种方法。
- 初始化
dp
数组。它的 长度为c+1
,dp[0] = 1
,其他初始值都为0
。
在初始化的时候 dp[0]
一定要初始化为 1,因为 dp[0]
是在公式中一切递推结果的起源,如果 dp[0]
是 0 的话,之后所有递推结果将都是 0。
- 遍历所有物品,倒序遍历背包容量(从
dp[c]
遍历到dp[w[i]]
,因为小于w[i]
的背包容量是不会发生变化的)。
# 遍历物品
for i in range(len(w)):
# 倒序遍历背包容量
for j in range(c, w[i] - 1, -1):
dp[j] = dp[j] + dp[j - w[i]]
这个递归方程的意思是,如果我们要用若干个元素组成和为 j
的方案数,那么有两种选择:不选第 i
个元素或者选第 i
个元素。如果不选第 i
个元素,那么原来已经有的dp[j]
种方案数就不变;如果选第 i
个元素,那么剩下要组成和为 j - w[i]
的方案数就等于 dp[j - w[i]]
。所以两种选择相加就是新的 dp[j]
。
由此角度衍生出的问题¶
- 494. 目标和 是求给定背包容量,装满背包有多少种方法。
完全背包¶
完全背包的大部分步骤与 01 背包完全一致,只需要在遍历背包容量时将倒序遍历改为正序遍历即可,因为每个物品可以无限次使用。
问题角度 1:给定背包容量,最多能装多少价值的物品?¶
dp = [0 for _ in range(c + 1)]
# 遍历物品
for i in range(len(w)):
# 顺序遍历背包容量
for j in range(w[i], c + 1):
dp[j] = max(dp[j], dp[j - w[i]] + p[i])
问题角度 2:给定背包容量,装满背包有多少种方法?(求组合数)¶
dp = [0 for _ in range(c + 1)]
dp[0] = 1
# 遍历物品
for i in range(len(w)):
# 顺序遍历背包容量
for j in range(w[i], c + 1):
dp[j] = dp[j] + dp[j - w[i]]
由此角度衍生出的问题¶
- 518. 零钱兑换 II 是求给定背包容量,装满背包有多少种方法(求组合数)。
问题角度 3:给定背包容量,装满背包有多少种方法?(求排列数)¶
求排列数时,需要将背包容量放在外循环,将物品放在内循环。
dp = [0 for _ in range(c + 1)]
dp[0] = 1
# 遍历背包容量
for j in range(c + 1):
# 遍历物品
for i in range(len(w)):
if w[i] <= j:
dp[j] = dp[j] + dp[j - w[i]]
由此角度衍生出的问题¶
- 0377. 组合总和 Ⅳ 是求给定背包容量,装满背包有多少种方法(求排列数))。
- 70. 爬楼梯 给定总台阶数,每次可以爬 1 阶或 2 阶,求一共有多少种爬楼梯的方法。这道题当然可以用简单的斐波那契数列进行计算,但如果每次可以爬的台阶数太多,就更适合用完全背包问题求解,两层循环不断累加即可。
总结
如果求组合数就是外层 for 循环遍历物品,内层 for 遍历背包。
如果求排列数就是外层 for 遍历背包,内层 for 循环遍历物品。
问题角度 4:给定背包容量,装满背包最少需要多少件不同的物品?¶
-
确定
dp
数组的定义。凑足总额为 j 所需钱币的最少个数为 dp[j]在一维
dp
数组中,dp[j]
表示:容量为j
的背包,所背的不同物品数量最小为dp[j]
。 -
初始化
dp
数组。它的 长度为c+1
,dp[0]
初始值为 0,其他初始值都为float("inf")
或者c + 1
。这里可以用float("inf")
代表无穷大;也可以用c + 1
,因为若硬币的最小价值是 1,则不可能需要c+1
个硬币来组成c
的价值。 -
遍历所有物品,正序遍历背包容量。
递归逻辑是:要么不加入新的物品,需要
dp[j]
件物品才能凑成j
的金额;要么加入新的物品,需要dp[j - w[i]] + 1)
件物品才能凑成j
的金额。取两者 最小值 即可。
由此角度衍生出的问题¶
- 322. 零钱兑换 是求给定背包容量,装满背包最少需要多少件不同的物品。
- 279.完全平方数 是求给定背包容量,装满背包最少需要多少件不同的物品。需要将待求和的整数 n 看成背包容量,再用\(1^2\)、\(2^2\)、\(3^2\)一直到
(int(n**0.5)+1)**2
这些完全平方数作为物品,做完全背包。