题目
给你一个字符串 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
题解一 DFS+记忆化
var wordBreak = function(s, wordDict) {
const len = s.length;
const wordSet = new Set(wordDict);
const memo = new Array(len);
const canBreak = start => {
if(start === len) return true;//划分到最后一个了,不用再分了
if(memo[start] !== undefined) return memo[start];
for(let i = start+1;i <=len;i++){
let sub = s.slice(start,i);
if(wordSet.has(sub)&&canBreak(i)){
memo[start]=true;//缓存当前递归的结果
return true;
}
}
memo[start]=false;
return false;//后面的都不行了才为false
}
return canBreak(0);
};
笔记:
-
用 DFS 回溯,考察所有的拆分可能,指针从左往右扫描:
如果指针的左侧部分是单词,则对剩余子串递归考察。
如果指针的左侧部分不是单词,不用看了,回溯,考察别的分支。 -
memo[i]的意思是字符串s在i之后的子串可以break
-
array.slice(start,end)前开后闭
-
js 中 Set 常用方法总结
1、创建 Set
1.1、new Set()方式创建
let v = new Set()
1.2、通过传入数组方式创建let v = new Set([1,2,3,3,5])
2、添加元素
使用 add 方法
let v = new Set()
v.add(1)3、删除元素
使用 delete 方法
let v = new Set()
v.add(1)
v.add(5)
v.delete(5)4、获取 Set 中元素个数
使用 size
let v = new Set()
v.add(1)
v.add(5)
v.size5、判断 Set 中是否包含某个元素
使用 has 方法
let v = new Set()
v.add(1)
v.add(5)
v.has(5)6、遍历 Set
6.1、forEach形式
let v = new Set()
v.add(1)
v.add(5)v.forEach(t=>{
console.log(t)
})
6.2、for of 形式let v = new Set([1,2,3,3,5])
for(let t of v) {
console.log(t)
}7、Set 转换成数组
使用 Array.from 方法
let v = new Set([1,2,3,3,5])
let a = Array.from(v)
题解二 BFS
const wordBreak = (s, wordDict) => {
const wordSet = new Set(wordDict);
const len = s.length;
const visited = new Array(len);
const queue = [];
queue.push(0);
while (queue.length) {
const start = queue.shift(); // 考察出列的指针 存的是数组的index
if (visited[start]) continue; // 是访问过的,跳过
visited[start] = true; // 未访问过的,记录一下
for (let i = start + 1; i <= len; i++) { // 用指针i去划分两部分
const prefix = s.slice(start, i); // 前缀部分
if (wordSet.has(prefix)) { // 前缀部分是单词
if (i < len) { // i还没越界,还能继续划分,让它入列,作为下一层待考察的节点
queue.push(i);
} else { // i==len,指针越界,说明s串一路被切出单词,现在没有剩余子串,不用划分,返回true
return true;
}
} // 前缀部分不是单词,i指针不入列,继续下轮迭代,切出下一个前缀部分,再试
}
}
return false; // BFS完所有节点(考察了所有划分的可能)都没返回true,则返回false
};
笔记:
- 栈 push入 pop出
- 队列(queue)shift移除第一项并返回 unshift在前端添加任意项并返回新数组的长度
- bfs利用队列 父先进,然后父出,所有的子进(考察子节点,能入列的就入)。
- bfs要单独创建队列 dfs用栈
题解三 动态规划
var wordBreak = function (s, wordDict) {
const wordSet = new Set(wordDict);
const len = s.length;
const dp = new Array(len + 1).fill(false);
dp[0] = true;
for (let i = 1; i <= len; i++) {
for (let j = i - 1; j >= 0; j--) {// j去划分成两部分
if (dp[i] == true) break;
if (dp[j] == false) continue;
const suffix = s.slice(j, i); // 后缀部分 s[j: i-1]
if (wordSet.has(suffix) && dp[j] == true) { // 后缀部分是单词,且左侧子串[0,j-1]的dp[j]为真
dp[i] = true;
break;// dp[i] = true了,i长度的子串已经可以拆成单词了,不需要j继续划分子串了
}
}
}
return dp[s.length];
};
笔记:
-
-
s 串能否分解为单词表的单词(前 s.length 个字符的 s 串能否分解为单词表单词)
-
将大问题分解为规模小一点的子问题:
-
前 i个字符的子串,能否分解成单词
-
剩余子串,是否为单个单词。
dp[i]:长度为i的s[0:i-1]子串是否能拆分成单词。题目求:dp[s.length]
-
-
-
状态转移方程
- 类似的,我们用指针 j 去划分s[0:i] 子串,如下图:
- s[0:i] 子串对应 dp[i+1] ,它是否为 true(s[0:i]能否 break),取决于两点:
- 它的前缀子串 s[0:j-1] 的 dp[j],是否为 true。
- 剩余子串 s[j:i],是否是单词表的单词。
-
动态规划 优化 代码
迭代过程中,如果发现dp[i] == true ,直接break
如果dp[j] == false,dp[i]没有为 true 的可能,continue,考察下一个 j -
拆分时可以重复使用字典中的单词,说明就是一个完全背包.
-
01背包的核⼼代码
01背包内嵌的循环是从⼤到⼩遍历,为了保证每个物品仅被添加⼀次。 -
完全背包核心代码
完全背包的物品是可以添加多次的,所以要从⼩到⼤去遍历