题目链接 | 理论基础
乍一看是回溯问题,和分割回文子串很像,不过本题使用回溯解决会超时(有些极端 case 过不了),而且这样只需要求解 True/False 的问题一般不会考虑回溯,毕竟回溯是暴力的指数搜索。
将 wordDict
看作可以无限取用的物品,s
看作是背包,问能否用物品填满背包 – 完全背包,启动!
注意到本题的结果是必须依赖于排列的,只靠组合不能确定结果,因为填满背包的方式是有顺序要求的,同样是 wordDict = ["apple", "pen"]
,["apple", "pen", "apple"]
就可以得到 "applepenapple"
,而 ["apple", "apple", "pen"]
不可以。用爬楼梯的思路来解决的话会流畅一些。
dp 数组的下标含义:dp[j]
代表是否能够填满背包 s[:j]
dp 递推公式:dp[j] = dp[j] or dp[j - len(wordDict[i])]
wordDict[i]
,就已经能组合成 s[:j]
wordDict[i]
,并且之前已经能组成 s[:j-len(wordDict[i])]
s[j - len(wordDict[i]): j] == wordDict[i]
成立dp[j]=True
dp 数组的初始化:根据递推公式可以得到 dp[0]=True
,否则后面的递推无法进行
dp[0]
的取值没有明确定义,但是 dp[0]=True
能够使得 wordDict = ["pen"], s = "pen"
的情况推导出正确的 dp 数组dp 的遍历顺序:如上所述,这道题的结果依赖于排列而非组合,很明显必须是先背包再物品(爬楼梯 yyds)
举例推导:wordDict = ["leet", "code"], s = "leetcode"
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|---|
dp | T | F | F | F | T | F | F | F | T |
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
# dp[j] represents whether can make s[:j]
dp = [False] * (len(s) + 1)
dp[0] = True
# dp formula, bag->item
for j in range(1, len(s) + 1):
for i in range(len(wordDict)):
if j >= len(wordDict[i]) and s[j - len(wordDict[i]): j] == wordDict[i]:
dp[j] = dp[j] or dp[j - len(wordDict[i])]
return dp[-1]
理论基础
给定一个限重为 V V V 的背包,有 N N N 种物品,其中第 i i i 件物品有 M i M_i Mi 个可用,每一件消耗空间 C i C_i Ci,价值是 W i W_i Wi。求怎样装入物品,使得物品总重量不会超过背包限重,同时获得最大价值。
最简单的等价转换就是将多重背包转化为 01 背包问题。将每种物品每一件都摊开来,就是 01 背包,毕竟 01 背包也没有规定物品的重量、价值不能相同。
如下,第一张表是完全背包,第二张表是 01 背包:
物品价值 | 物品重量 | 数量 | |
---|---|---|---|
物品 0 | 1 | 15 | 1 |
物品 1 | 3 | 20 | 3 |
物品 2 | 4 | 30 | 2 |
物品价值 | 物品重量 | |
---|---|---|
物品 0 | 1 | 15 |
物品 1 | 3 | 20 |
物品 1 | 3 | 20 |
物品 1 | 3 | 20 |
物品 2 | 4 | 30 |
物品 2 | 4 | 30 |
所以,只要将多重背包正确地转换成 01 背包的输入,就可以用 01 背包的方法来解决。转换代码如下:
weight = [1, 3, 4]
value = [15, 20, 30]
nums = [2, 3, 2]
bagWeight = 10
# 将数量大于1的物品展开
for i in range(len(nums)):
while nums[i] > 1:
weight.append(weight[i])
value.append(value[i])
nums[i] -= 1
另一种思路就是在物品+背包的遍历内部再加上一层使用数量的遍历。之前的 01 背包,无论是二维数组还是滚动数组,无论遍历顺序,都需要考虑“当前背包容量为 j,是否要取物品 i”;现在的滚动背包内,需要考虑“当前背包容量为 j,要取多少件物品 i”。
本质上还是很像爬楼梯。
def test_multi_pack(weight, value, nums, bagWeight):
dp = [0] * (bagWeight + 1)
for i in range(len(weight)): # 遍历物品
for j in range(bagWeight, weight[i] - 1, -1): # 遍历背包容量
# 以上为01背包,然后加一个遍历个数
for k in range(1, nums[i] + 1): # 遍历个数
if j - k * weight[i] >= 0:
dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i])
# 使用 join 函数打印 dp 数组
print(' '.join(str(dp[j]) for j in range(bagWeight + 1)))
print(dp[bagWeight])
if __name__ == "__main__":
weight = [1, 3, 4]
value = [15, 20, 30]
nums = [2, 3, 2]
bagWeight = 10
test_multi_pack(weight, value, nums, bagWeight)
理论基础
通过之前这一堆背包问题的练习,解决背包问题已经比较有套路了,对遍历顺序、初始化的理解也算不错。然而,将复杂的问题背景抽象成背包问题,仍然是需要经过思考的,其中最后一块石头的重量 II、目标和 绝对是这种复杂抽象的难题。
另外,很多背包问题看上去也都能用回溯算法解决,但毫无疑问都一定会超时。区别在于,背包问题依然能够依靠子问题的解来节省复杂度,而回溯算法不可避免地需要进行穷举,只不过是优雅的穷举,两者还是有本质上的区别。
代码随想录上总结了不同的递推公式。但这些背包问题刷下来,个人感觉找对了 dp 数组的含义后,dp 递推公式就能很自然地得到,无需特别考虑。相比之下,初始化和遍历顺序才是更大的坑。
背包问题的初始化堪称是五花八门,每一道题都需要谨慎思考。找到正确的初始化,最好的方法是对于自己的想法,找个简单的例子推一下,看看能否得到想要的答案。
dp[0]
。这种情况下,最重要的是确保自己的初始化能够正确地得到后续的结果即可。dp[i][j]
的时候要用到 dp[i-1][...]
的值。二维数组的初始化一般比较直观,能够根据含义直接得到 i=0 和 j=0 时的值,通常来说也能帮助顺利得到正确的递推结果。
dp[0]
,要思考的东西大大减少了。相对应的,滚动数组的初始化更为抽象,因为这里的初始化是没有发生更新之前,需要结合递推公式来得到合理的初始化值。