0%

LeetCode.139.单词拆分(中等)

题目

LeetCode.139.单词拆分(中等)

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

  • 拆分时可以重复使用字典中的单词。
  • 你可以假设字典中没有重复的单词。

题解

解法1:递归

提到拆分,我们可以考虑能不能把大问题拆分为子问题+有限步骤,给问题建模,简化讨论。

  1. 如果s可以被拆分,那么至少s的末尾字符组成的单词肯定是在wordDict中的。
    • s的末尾字符组成的单词,可以从s的末尾位置开始,依次穷举取长度为1、2、3……n的子字符串,看子字符串是否在wordDict中。
    • 接下来再去看剩下的前半部分的子字符串能不能继续拆分成功。
  2. 如果s不能被拆分,有两种情况:
    1. 穷举s所有末尾子字符串,是没有一个子字符串是在wordDict中的。
    2. 末尾有一部分子字符串组成的单词在wordDict中,最后s剩下前半段的子字符串不能被wordDict拆分。

这里已经划分出了子问题+有限步骤,可以用递归来解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
fun wordBreak(s: String, wordDict: List<String>): Boolean {
fun canBreak(end: Int): Boolean {
// 如果字符串能拆完,最后索引肯定为-1
if (end < 0) return true
var result = false
// 在字符串s[0,end]中,从末尾依次取长度为1、2、3……end+1的子字符串,检查子字符串是否在wordDict里
for (start in end downTo 0) {
// 截取末尾字符串
val tailString = s.substring(start, end + 1)
// 末尾字符串在wordDict里
// 剩下的前半段子字符串s[0,start - 1]有可能可以被继续拆分,也有可能不可以被拆分,不知道能不能就只能每个情况都试一下
// 所以这里不是直接return,而是要试一下每一种的单词拆分,看哪一种行的通
if (wordDict.contains(tailString)) {
// end指向剩下的前半部分子字符串末尾
result = canBreak(start - 1) || result
}
}
return result
}
// 尝试拆分s[0, n - 1]
return canBreak(s.length - 1)
}
}

解法2:记忆化递归

递归的问题在于有重叠子问题,有很多重复计算耗时,保存一下中间计算结果就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
fun wordBreak(s: String, wordDict: List<String>): Boolean {
val memo = IntArray(s.length) { -1 }
fun canBreak(end: Int): Boolean {
if (end < 0) return true
if (memo[end] != -1) {
return if (memo[end] == 1) true else false
}
var result = false
for (start in end downTo 0) {
val tailString = s.substring(start, end + 1)
if (wordDict.contains(tailString)) {
result = canBreak(start - 1) || result
}
}
memo[end] = if (result) 1 else 0
return result
}
return canBreak(s.length - 1)
}
}

解法3:自下而上递推式动态规划

能用记忆化递归的,也可以用自下而上递推的方式做。

dp[end]为第0个到第end个字符组成的字符串是否可以被wordDict中的单词拆分。
穷举所有s[0, end]的末尾字符串,判断能否拆分。
s[0, end]的末尾字符串的开始索引为start0 <= start <= end

状态转移方程
dp[end] = dp[start - 1] && s[start, end] in wordDict

start==0 时,截取的是整个字符串,直接判断整个字符串是否在wordDict里就行了,dp[0] = s[start, end] in wordDict

由于计算新的状态需要读取之前的每个最优化的状态,所以空间无法优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
fun wordBreak(s: String, wordDict: List<String>): Boolean {
val n = s.length
if (n == 0) return false
val dp = BooleanArray(n) { false }
for (end in 0 until n) {
for (start in 0..end) {
val word = s.substring(start, end + 1)
if (start == 0) {
if (word in wordDict) {
dp[end] = true
break
}
} else {
if (dp[start - 1] && word in wordDict) {
dp[end] = true
break
}
}
}
}
return dp[n - 1]
}
}