139. 单词拆分
1、题目
给你一个字符串 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 <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s
和wordDict[i]
仅由小写英文字母组成wordDict
中的所有字符串 互不相同
- 字典树
- 记忆化搜索
- 数组
- 哈希表
- 字符串
- 动态规划
2、题目分析
动态规划核心用处是求最值,当然也可用于判断是否存在可行性方案这类的问题。因为是先有多个可行性方案,才会衍生出最值问题。
故该问题也可以用动态规划,虽然题目只求截止到最后一个字母,字典中出现的单词是否能拼接出该字母及前面所组成的字符串 s。
动态规划的解决套路可分为 2 步,①基于问题能定义出状态,②状态间具备动态规划的三个特性
①基于问题定义出状态:问题是“求截止到最后一个字母所组成的字符串,是否可由字典拼接而成”。则状态就可以定义为“截止到某个字母所组成的字符串,是否可由字典拼接而成”。因为这样,当流转到最终状态时,即可得到问题的答案。
②状态间具备动态规划的三个特性
- 重复子问题且重复策略
重复子问题:截止到每个字母,问题都是“字典中出现的单词是否能拼接出截止到该字母所组成的字符串” - 最优子结构
重复策略&最优子结构:针对每个子问题,策略都是以该字母做为尾字符,匹配字典中的每个单词,然后往前推到该单词加入前的状态,若单词加入前的状态已被求得解。则当前状态就有解。否则,当前状态也无解。 - 无后效性
更前的状态不影响当前状态:从2、最优子结构可知,我们求当前状态时,只关注单词加入前的状态,而无需关注单词加入前的状态是如何被一步步推导出来的。
后面的状态不影响当前状态:从2、最优子结构可知,当前状态完全有前面的状态转移得到,跟后面的状态没有关系
3、解题步骤
1、dp[i] 表示截止到第 i 个字符,所组成的字符串,是否可由字典拼接而成。
2、初始化,dp[0]表示空串,默认可由字典拼接而成
3、迭代处理截止到的每个字符所组成的字符串,判断字符串是否可由字典拼接而成
4、判断当前状态是否可达。即是否存在前面的某个状态,结合上本单词 word,转移到当前状态
5、若以当前字符为尾字符,在字典中找到了匹配字符串的后缀。再因为开头已经判断了结合该单词前的那个状态dp值为1,即前面状态可达,故当前状态可达。
4、复杂度最优解代码示例
public boolean wordBreak(String s, List<String> wordDict) {
// 1、dp[i] 表示截止到第 i 个字符,所组成的字符串,是否可由字典拼接而成。
boolean[] dp = new boolean[s.length() + 1];
// 2、初始化,dp[0]表示空串,默认可由字典拼接而成
dp[0] = true;
for (int i = 1; i <= s.length(); i++) {
// 3、迭代处理截止到的每个字符所组成的字符串,判断字符串是否可由字典拼接而成
for (String word : wordDict) {
// 4、判断当前状态是否可达。即是否存在前面的某个状态,结合上本单词 word,转移到当前状态
if (i >= word.length() && dp[i - word.length()] && s.startsWith(word, i - word.length())) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
5、抽象与扩展
通用动态规划的解法,见标题二