题目
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 说明:
1.拆分时可以重复使用字典中的单词。
2.你可以假设字典中没有重复的单词。
示例 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. 定义状态
2. 初始状态
3. 状态转移方程
4. 从dp[]中获取结果
具体到题目
定义状态
定义数组dp[i]表示字符s的0到i-1位置的子字符串是否能由wordDict中的单词组成
初始状态
dp[0]表示的是空字符""能否由wordDict中的单词组成,默认是成立的,因此dp[0]=true
状态转移方程
dp[i] 需要遍历wordDict每个单词word,根据word的长度p一起获得,以示例 1为例: 当i=4时,遍从dp[]中获取结果历wordDict的第一个元素leet,得到p=4,因此p[4] = p[0]+ s.substring(4-4,4) === 'leet'。
从dp[]中获取结果
dp[]的最后一项就是最终结果
动态规划完整代码
var wordBreak = function (s, wordDict) {
const len = s.length;
const dp = new Array(len + 1).fill(false);
dp[0] = true;
for (let i = 1; i < len + 1; i++) {
for (let j = 0; j < wordDict.length; j++) {
if (dp[i] === true) {
break;
}
const cmplen = wordDict[j].length;
if (cmplen > i) {
continue;
}
const str = s.substring(i - cmplen, i);
console.log(str);
dp[i] = dp[i - cmplen] && str === wordDict[j];
}
}
console.log(dp);
return dp.pop();
};
记忆化回溯
刚开始的时候不知道怎么根据动态规划建模,所以看完题目的第一个想法就是回溯。 刚开始的代码如下
var wordBreak = function (s, wordDict) {
const res = false;
const backtrack = (string) => {
if (string === '') {
res = true;
return;
}
for (let i = 0; i < wordDict.length; i++) {
const helpStr = string.substring(index);
if (helpStr.startsWith(wordDict[i])) {
const match = wordDict[i];
const matchLen = match.length;
backtrack(s.substring(matchLen));
}
};
return backtrack(s, 0);
};
想法思路是通过不断是裁剪字符串s,使得最后得到空字符串"",结果就是true。示例能跑过,但是碰到
s =
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab';
wordDict = [
'a',
'aa',
'aaa',
'aaaa',
'aaaaa',
'aaaaaa',
'aaaaaaa',
'aaaaaaaa',
'aaaaaaaaa',
'aaaaaaaaaa'
];
就超时了。
分析原因就是当第一个a计算了两次后的结果其实和aa计算第一次的结果是重复的,即如果i位置已经计算过了,那么结果不需要再重复计算一遍。所以需要引入记忆化数组dp来记录i位置是否已经计算过并且能否满足前i个位置组成的字符串能否被wordDict拆分。
记忆化回溯完整代码
var wordBreak = function (s, wordDict) {
const dp = new Array(len).fill(false);
const backtrack = (string, index) => {
if (index === string.length) {
return true;
}
if (dp[index]) {
return false;
}
for (let i = 0; i < wordDict.length; i++) {
const helpStr = string.substring(index);
if (helpStr.startsWith(wordDict[i])) {
const match = wordDict[i];
const matchLen = match.length;
if (backtrack(s, index + matchLen)) {
return true;
}
}
}
dp[index] = true;
return false;
};
return backtrack(s, 0);
};
总结
本道题目顺便复习了一下回溯算法的基本思路,即
result = [];
def backtrack(path, list)
if(满足条件){
result.push(path)
}
for 选择 in list
做选择
backtrack(path, list)
撤销选择
本道题目动态规划的难点我感觉是建模,就是理解用数组dp表示前i个字符串能否由wordDict拆分成功。需要多总结经验