这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战
单词拆分(题号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
链接
解释
这题啊,这题是经典DP。
一开始真没想到DP,一开始比较直接,直接一层for循环,利用额外的一个变量来存储当前单词的内容,如果当前单词在字典里面,就重置单词,这样如果循环完成后当前单词不为空,那就返回false,如果干到最后变为空了,那就证明这个字符串可以由字典中的单词构成。
代码也很简单👇:
var wordBreak = function(s, wordDict) {
const set = new Set(wordDict)
let word = ''
for (const char of s) {
word += char
if (set.has(word)) {
word = ''
}
}
return !word
};
结果一个测试用例给我干懵了,就是这个:
"aaaaaaa"
["aaaa","aaa"]
为什么在这个测试用例会崩?其实也很简单了,因为原字符串应该是由aaaa和aaa构成的,但根据上面代码的逻辑,遇到aaa会截断一次,那么此时字符串还剩下aaaa,遇到aaa会再截断一次,最后就剩一个a了,然后GG。
问题就出现在遇到第四个字符的时候,其实应该可以继续走的,不应该直接截断,那么此时就想到了第二种方案——递归。
利用递归来走那些没尝试过的可能性,这样覆盖面就广了。
具体的放到答案里进行解释了,此处就不占用篇幅了。
用了递归之后再看看官方答案,其实DP也是可以解决的,并且好像更简单些。
DP的思路也很简单,用dp[i]来分割数组,如果dp[0~i]和dp[i~n]都满足字典的条件,那就证明当前的dp[i]是合格的,然后再拆分i,用循环来一点点找到0~j和j~i的组合是不是符合条件,如果符合条件,证明dp[i]符合条件。
讲真,还是比较好理解的,而且整体思路也比较符合DP的整体思路,首先是递归,递归之后利用记忆化进行优化,最后推导出DP。
自己的答案(递归)
递归的思路也比较简单,为了避免错过的问题,得考虑到所有的可能性。
所以需要从数组的所有位置开始查找,如果当前单词在字典里,判断剩余的部分是否都在字典里,如果在就返回true,如果全都不在就返回false。
var wordBreak = function(s, wordDict) {
const len = s.length
const set = new Set(wordDict)
function DFS(start) {
if (start === len) return true
for (let i = start + 1; i <= len; i++) {
if (set.has(s.slice(start, i)) && DFS(i)) return true
}
return false
}
return DFS(0)
}
答案显然没有这么简单,在执行到第34个巨长的测试用例时GG了,原因很简单,里面有很多数据进行了重复计算,浪费了性能。
自己的答案(递归+记忆化)
要想优化递归,首先得弄明白重复数据出现在哪里,看看👆的代码会发现,i会有大量的重复情况出现,举个例子:
"aaaaaaa"
["aaaa","aaa"]
一开始i肯定尝试了0~6,遇到2的时候start变成了3开始走,在这里会直接成功,但如果失败了呢?会走到下一次for循环中去,那么会再一次尝试到重复的失败数字,所以这里应该用一个东西来记录当前尝试过的结果,不管是失败还是成功,避免后续的重复计算。
var wordBreak = function(s, wordDict) {
const len = s.length
const set = new Set(wordDict)
const map = new Map()
function DFS(start) {
if (start === len) return true
if (map.has(start)) return map.get(start)
for (let i = start + 1; i <= len; i++) {
if (set.has(s.slice(start, i)) && DFS(i)) {
map.set(i, true)
return true
}
}
map.set(start, false)
return false
}
return DFS(0)
}
简简单单的用Map来记录即可,实现记忆化的操作,如此便可以走通所有的测试用例了。
更好的方法(动态规划)
DP的思路在解释中已经说过了,直接看代码:
var wordBreak = function(s, wordDict) {
const len = s.length
const set = new Set(wordDict)
const dp = new Array(len + 1).fill(false)
dp[0] = true
for (let i = 0; i <= len; i++) {
for (let j = 0; j < i; j++) {
if (dp[j] && set.has(s.substr(j, i - j))) {
dp[i] = true
break
}
}
}
return dp[len]
};
主要就是将可能性拆解到dp数组中的每个元素,之后再将i拆解为j来进行分段处理,理解起来还是比较简单的。
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇