# [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
这道题最暴力的想法是,我们可以直接用回溯法来解。
遍历所有字典,当发现字典匹配字符串的前缀时,去掉该前缀,并拿剩余字符串,用字典进行递归匹配。
直到剩余字符串等于字典中某一单词时,说明字符串完全匹配。此时返回true。
function wordBreak(s: string, wordDict: string[]): boolean {
let res = false;
for (let i = 0; i < wordDict.length; i++) {
if (res) return true;
backtrack(s, i);
}
return res;
function backtrack(str: string, index: number) {
const word = wordDict[index];
// 剩余字符串不以word开头,剪枝
if (str.indexOf(word) !== 0) return;
// 匹配完毕
if (str === word) {
res = true;
return;
}
for (let i = 0; i < wordDict.length; i++) {
backtrack(str.slice(word.length), i);
}
}
}
但这样做相当暴力,会直接导致栈溢出。我们需要找到一个更好复杂度的解法。
我们发现该题实际上是一个背包问题,并且是一个完全背包问题。
其存在最优子问题。即我们可以从后往前推导,若一个单词与等长字符串尾缀相等时,且去除尾缀后的字符串能够被字典拼出,则该字符串能被字典拼出。
因此我们设 dp[i] 表示字符串s的前 i 位是否可以用字典中的单词表示。
对于字典中的每一个词word,想办法找出dp[i]与dp[i-word.length]的关系。
若i < word.length,则一定是无解的
若截取子序列尾部能匹配字典中的单词,并且dp[n-word.length]有解,则该子序列有解,解为:dp[n-word.length]的解加上当前匹配单词
特殊的,当s为空时,任何情况下都匹配,即dp[0]=true
状态转移方程:dp[i] = dp[i - word.length] && s.substring(i - word.length, i) === word
function wordBreak(s: string, wordDict: string[]): boolean {
const dp = Array(s.length + 1).fill(0);
dp[0] = 1;
for (let i = 1; i <= s.length; i++) {
for (const word of wordDict) {
// 剪枝
if (i < word.length) continue;
const tempWord = s.substring(i - word.length, i);
if (tempWord === word && dp[i - word.length]) {
dp[i] = 1;
break;
}
}
}
return dp[s.length];
}