139. 单词拆分
动态规划
-
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
-
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。
所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
-
从递归公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递归的根基,dp[0]一定要为true,否则递归下去后面都都是false了。下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。
-
题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。本题最终要求的是是否都出现过,所以对出现单词集合里的元素是组合还是排列,并不在意!
# Time complexity: O(N^3)
# Space complexity: O(N)
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
dp = [False] * (len(s) + 1)
dp[0] = True
for i in range(1, len(s)+1):
for word in wordDict:
word_len = len(word)
if i >= word_len:
dp[i] = dp[i] or (dp[i-word_len] and s[i-word_len:i] == word)
return dp[-1]
多重背包
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
多重背包和01背包是非常像的, 为什么和01背包像呢?每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。
例如:
背包最大重量为10。
物品为:
| 重量 | 价值 | 数量 | |
|---|---|---|---|
| 物品0 | 1 | 15 | 2 |
| 物品1 | 3 | 20 | 3 |
| 物品2 | 4 | 30 | 2 |
问背包能背的物品最大价值是多少?
和如下情况有区别么?
| 重量 | 价值 | 数量 | |
|---|---|---|---|
| 物品0 | 1 | 15 | 1 |
| 物品0 | 1 | 15 | 1 |
| 物品1 | 3 | 20 | 1 |
| 物品1 | 3 | 20 | 1 |
| 物品1 | 3 | 20 | 1 |
| 物品2 | 4 | 30 | 1 |
| 物品2 | 4 | 30 | 1 |
毫无区别,这就转成了一个01背包问题了,且每个物品只用一次。
这种方式来实现多重背包的代码如下:
def test_multi_pack1(bag_size, weights, values, nums):
dp = [0] * (bag_size + 1)
for i in range(len(nums)):
while nums[i] > 1:
weights.append(weights[i])
values.append(values[i])
nums[i] -= 1
for i in range(len(values)):
cur_weight, cur_val = weights[i], values[i]
for j in range(bag_size, cur_weight - 1, -1):
dp[j] = max(dp[j], dp[j - cur_weight] + cur_val)
return dp[-1]
if __name__ == "__main__":
weights = [1, 3, 4]
values = [15, 20, 30]
nums = [2, 3, 2]
bag_size = 10
print(test_multi_pack1(bag_size, weights, values, nums))
也有另一种实现方式,就是把每种商品遍历的个数放在01背包里面在遍历一遍。
从代码里可以看出是01背包里面在加一个for循环遍历一个每种商品的数量。 和01背包还是如出一辙的。
# 时间复杂度:O(m × n × k),m:物品种类个数,n背包容量,k单类物品数量
def test_multi_pack2(bag_size, weights, values, nums):
dp = [0] * (bag_size + 1)
for i in range(len(values)):
cur_weight, cur_val = weights[i], values[i]
for j in range(bag_size, cur_weight - 1, -1):
for k in range(1, nums[i] + 1):
if j - k * cur_weight >= 0:
dp[j] = max(dp[j], dp[j - k * cur_weight] + k * cur_val)
return dp[-1]
if __name__ == "__main__":
weights = [1, 3, 4]
values = [15, 20, 30]
nums = [2, 3, 2]
bag_size = 10
print(test_multi_pack2(bag_size, weights, values, nums))
背包问题总结
背包递推公式
- 问能否能装满背包(或者最多装多少):
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); - 问装满背包有几种方法:
dp[j] += dp[j - nums[i]]; - 问背包装满最大价值:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); - 问装满背包所有物品的最小个数:
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
遍历顺序
01背包
- 二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
- 一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
- 一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!
完全背包
- 纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
- 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
- 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
- 如果求最小数,那么两层for循环的先后顺序就无所谓了