导语
leetcode刷题笔记记录,本篇博客是动态规划部分,主要记录题目包括:
Leetcode 139. 单词拆分
题目描述
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意: 不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
提示:
1 <= s.length <= 3001 <= wordDict.length <= 10001 <= wordDict[i].length <= 20s和wordDict[i]仅由小写英文字母组成wordDict中的所有字符串 互不相同
解法
使用动态规划转换为完全背包问题:要表示的字符串为背包,每个单词为物品,那么应用动规五部曲如下:
- dp数组含义:
dp[i]表示字符串s的前i个字符能否被字典wordDict中的单词拼接而成,是bool类型的值; - 递推公式,结合下面的示意图来理解:如果dp[j]为True,而且[j:i]组成的子串是一个单词,那么dp[i]更新为True;
- 初始化:dp[0]为True,其他均为False;
- 遍历顺序:由于这里需要求解的是组成方式,那么求解的是“排列”的情形,应该先遍历背包,后遍历物品;
- 打印dp数组。
代码如下:
from typing import List
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
# 初始化动态规划数组 dp,长度为 len(s) + 1。
# dp[i] 表示字符串 s 的前 i 个字符能否被字典 wordDict 中的单词拼接而成。
# dp[0] 是 True,因为空字符串总是可以被拼接。
dp = [True] + [False] * len(s)
# 外层循环:遍历 s 的所有子字符串。
for i in range(1, len(s) + 1):
# 内层循环:遍历当前子字符串的所有可能的起始位置。
for j in range(i):
# 获取当前子字符串。
word = s[j:i]
# 检查当前子字符串是否在 wordDict 中,并且 dp[j] 是 True。
# 如果是这样,那么 s 的前 i 个字符可以被拼接,所以设置 dp[i] 为 True。
if word in wordDict and dp[j]:
dp[i] = True
# 返回 dp[len(s)],它表示整个字符串 s 能否被拼接。
return dp[len(s)]
易错点
题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。需要讨论两层for循环的前后顺序。根据[代码随想录]中的总结:
如果求组合数就是外层for循环遍历物品,内层for遍历背包。 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
部分题目如下:
- 求组合数:动态规划:518.零钱兑换II (opens new window)
- 求排列数:动态规划:377. 组合总和 Ⅳ (opens new window)、动态规划:70. 爬楼梯进阶版(完全背包) (opens new window)
- 求最小数:动态规划:322. 零钱兑换 (opens new window)、动态规划:279.完全平方数(opens new window)
而本题其实我们求的是排列数,假设s = "applepenapple", wordDict = ["apple", "pen"]。"apple", "pen" 是物品,那么物品的组合一定是 "apple" + "pen" + "apple" 才能组成 "applepenapple"。强调物品之间顺序。所以说,本题一定是先遍历背包,再遍历物品。