英文原文
Given a string s
and a dictionary of strings wordDict
, return true
if s
can be segmented into a space-separated sequence of one or more dictionary words.
Note that the same word in the dictionary may be reused multiple times in the segmentation.
Example 1:
Input: s = "leetcode", wordDict = ["leet","code"] Output: true Explanation: Return true because "leetcode" can be segmented as "leet code".
Example 2:
Input: s = "applepenapple", wordDict = ["apple","pen"] Output: true Explanation: Return true because "applepenapple" can be segmented as "apple pen apple". Note that you are allowed to reuse a dictionary word.
Example 3:
Input: s = "catsandog", wordDict = ["cats","dog","sand","and","cat"] Output: false
Constraints:
1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s
andwordDict[i]
consist of only lowercase English letters.- All the strings of
wordDict
are unique.
中文题目
给你一个字符串 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
中的所有字符串 互不相同
通过代码
高赞题解
早安,今天是端午,这道题很经典,很多题有它的影子,一起啃下。
DFS 思路
"leetcode"
能否 break,可以拆分为:"l"
是否是单词表的单词、剩余子串能否 break。"le"
是否是单词表的单词、剩余子串能否 break。"lee"
…以此类推
用 DFS 回溯,考察所有的拆分可能,指针从左往右扫描:
如果指针的左侧部分是单词,则对剩余子串递归考察。
如果指针的左侧部分不是单词,不用看了,回溯,考察别的分支。
我画出递归树,即问题的解的空间树:
DFS 代码(看注释辅助理解)
通过23/36个用例,遇到下面这个用例超时:
“aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab,[“a”,”aa”,”aaa”,”aaaa”,”aaaaa”,”aaaaaa”,”aaaaaaa”,”aaaaaaaa”,”aaaaaaaaa”,”aaaaaaaaaa”]
const wordBreak = (s, wordDict) => {
const len = s.length;
const wordSet = new Set(wordDict);
const canBreak = (start) => { // 判断从start到末尾的子串能否break
if (start == len) {//指针越界,s一步步成功划分为单词,才走到越界这步,现在没有剩余子串
return true; //返回真,结束递归
}
for (let i = start + 1; i <= len; i++) { //指针i去划分两部分,for枚举出当前所有的选项i
const prefix = s.slice(start, i); // 切出的前缀部分
if (wordSet.has(prefix) && canBreak(i)) {// 前缀部分是单词,且剩余子串能break,返回真
return true;
} // 如果前缀部分不是单词,就不会执行canBreak(i)。进入下一轮迭代,再切出一个前缀串,再试
}
return false; // 指针i怎么划分,都没有返回true,则返回false
}
return canBreak(0); // 递归的入口,从0到末尾的子串能否break
}
func canBreak(start int, s string, wordMap map[string]bool) bool {
if start == len(s) {
return true
}
for i := start + 1; i <= len(s); i++ {
prefix := s[start:i]
if wordMap[prefix] && canBreak(i, s, wordMap) {
return true
}
}
return false
}
func wordBreak(s string, wordDict []string) bool {
wordMap := map[string]bool{}
for _, v := range wordDict {
wordMap[v] = true
}
return canBreak(0, s, wordMap)
}
加入记忆化
- 下面这个例子中,start 指针代表了节点的状态,可以看到,做了大量重复计算:
- 用一个数组,存储计算的结果,数组索引为指针位置,值为计算的结果。下次遇到相同的子问题,直接返回命中的缓存值,就不用调重复的递归。
DFS + 记忆化 代码
const wordBreak = (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]; // memo中有,就用memo中的
for (let i = start + 1; i <= len; i++) {
const prefix = s.slice(start, i);
if (wordSet.has(prefix) && canBreak(i)) {
memo[start] = true; // 当前递归的结果存一下
return true;
}
}
memo[start] = false; // 当前递归的结果存一下
return false;
};
return canBreak(0);
};
func canBreak(start int, s string, wordMap map[string]bool, memo map[int]bool) bool {
if start == len(s) {
return true
}
if res, ok := memo[start]; ok {
return res
}
for i := start + 1; i <= len(s); i++ {
prefix := s[start:i]
if wordMap[prefix] && canBreak(i, s, wordMap, memo) {
memo[start] = true
return true
}
}
memo[start] = false
return false
}
func wordBreak(s string, wordDict []string) bool {
wordMap := map[string]bool{}
for _, v := range wordDict {
wordMap[v] = true
}
memo := make(map[int]bool)
return canBreak(0, s, wordMap, memo)
}
BFS 方法
刚才我们用DFS遍历空间树,当然也能用BFS。
维护一个队列,依然用指针描述一个节点。
起初,指针 0 入列,然后它出列,指针 1,2,3,4,5,6,7,8 是它的子节点,它们分别与 0 围出前缀子串,若是单词,让它入列,留作考察以它为起点的剩余子串;若不是单词,对应的指针不入列
然后重复这么做:节点(指针)出列,考察它的子节点,能入列的就入列、再出列……
直到指针越界,没有剩余子串了,没有指针可入列,如果前缀子串是单词,说明之前一直在切出单词,返回 true。
如果整个BFS过程,始终没有返回true,则返回 false。
BFS 代码
const wordBreak = (s, wordDict) => {
const wordSet = new Set(wordDict);
const len = s.length;
const queue = [];
queue.push(0);
while (queue.length != 0) {
const start = queue.shift(); // 考察出列的指针
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
};
func wordBreak(s string, wordDict []string) bool {
l := len(s)
wordMap := map[string]bool{}
for _, v := range wordDict {
wordMap[v] = true
}
queue := []int{}
queue = append(queue, 0)
for len(queue) != 0 {
start := queue[0]
queue = queue[1:]
for i := start + 1; i <= l; i++ {
prefix := s[start:i]
if wordMap[prefix] {
if i < l {
queue = append(queue, i)
} else {
return true
}
}
}
}
return false
}
看似没有问题,但同样的,遇到下面的例子就超时。
BFS 避免访问重复的节点
未剪枝的DFS会搜索重复的子树,BFS也一样。你可以想想该用例,BFS是如何重复访问节点。
解决:用一个 visited 数组记录访问过的节点,出列考察一个指针时,存在重复则跳过。
优化后的 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(); // 考察出列的指针
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
};
func wordBreak(s string, wordDict []string) bool {
l := len(s)
wordMap := map[string]bool{}
for _, v := range wordDict {
wordMap[v] = true
}
queue := []int{}
queue = append(queue, 0)
visited := map[int]bool{}
for len(queue) != 0 {
start := queue[0]
queue = queue[1:]
if visited[start] {
continue
}
visited[start] = true
for i := start + 1; i <= l; i++ {
prefix := s[start:i]
if wordMap[prefix] {
if i < l {
queue = append(queue, i)
} else {
return true
}
}
}
}
return false
}
方法3:动态规划
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]
,是否是单词表的单词。
base case
base case 为
dp[0] = true
。即,长度为 0 的s[0:-1]
能拆分成单词表单词。这看似荒谬,但这只是为了让边界情况也能套用状态转移方程,而已。
当 j = 0 时(上图黄色前缀串为空串),
s[0:i]
的dp[i+1]
,取决于s[0:-1]
的dp[0]
,和,剩余子串s[0:i]
是否是单个单词。只有让
dp[0]
为真,dp[i+1]
才会只取决于s[0:i]
是否为单个单词,才能用上这个状态转移方程。
动态规划 代码
const wordBreak = (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去划分成两部分
const suffix = s.slice(j, i); // 后缀部分 s[j: i-1]
if (wordSet.has(suffix) && dp[j]) { // 后缀部分是单词,且左侧子串[0,j-1]的dp[j]为真
dp[i] = true;
break; // dp[i] = true了,i长度的子串已经可以拆成单词了,不需要j继续划分子串了
}
}
}
return dp[len];
};
func wordBreak(s string, wordDict []string) bool {
l := len(s)
wordMap := map[string]bool{}
for _, v := range wordDict {
wordMap[v] = true
}
dp := make([]bool, l+1)
dp[0] = true
for i := 1; i <= l; i++ {
for j := i - 1; j >= 0; j-- {
suffix := s[j:i]
if wordMap[suffix] && dp[j] {
dp[i] = true
break
}
}
}
return dp[l]
}
动态规划 优化 代码
迭代过程中,如果发现
dp[i] == true
,直接break如果
dp[j] == false
,dp[i]
没有为 true 的可能,continue,考察下一个 j
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--) {
if (dp[i] == true) break;
if (dp[j] == false) continue;
const suffix = s.slice(j, i);
if (wordSet.has(suffix) && dp[j] == true) {
dp[i] = true;
break;
}
}
}
return dp[s.length];
};
func wordBreak(s string, wordDict []string) bool {
wordMap := map[string]bool{}
for _, v := range wordDict {
wordMap[v] = true
}
dp := make([]bool, len(s)+1)
dp[0] = true
for i := 1; i <= len(s); i++ {
for j := i - 1; j >= 0; j-- {
if dp[i] == true {
break
}
if dp[j] == false {
continue
}
suffix := s[j:i]
if wordMap[suffix] && dp[j] {
dp[i] = true
break
}
}
}
return dp[len(s)]
}
本文在行文组织、用词表述上改了几十遍,一直在读改,你应该可以感受到这份真诚。可以点个赞鼓励一下。
最后修改于:2021-09-08
统计信息
通过次数 | 提交次数 | AC比率 |
---|---|---|
214176 | 413065 | 51.9% |
提交历史
提交时间 | 提交结果 | 执行时间 | 内存消耗 | 语言 |
---|
相似题目
题目 | 难度 |
---|---|
单词拆分 II | 困难 |