|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[139. 单词拆分](https://leetcode.cn/problems/word-break/solution/by-ac_oier-gh00/)** ,难度为 **中等**。 |
| 4 | + |
| 5 | +Tag : 「动态规划」、「哈希表」、「序列 DP」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给你一个字符串 `s` 和一个字符串列表 `wordDict` 作为字典。请你判断是否可以利用字典中出现的单词拼接出 `s` 。 |
| 10 | + |
| 11 | +注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。 |
| 12 | + |
| 13 | +示例 1: |
| 14 | +``` |
| 15 | +输入: s = "leetcode", wordDict = ["leet", "code"] |
| 16 | +
|
| 17 | +输出: true |
| 18 | +
|
| 19 | +解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。 |
| 20 | +``` |
| 21 | +示例 2: |
| 22 | +``` |
| 23 | +输入: s = "applepenapple", wordDict = ["apple", "pen"] |
| 24 | +
|
| 25 | +输出: true |
| 26 | +
|
| 27 | +解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。 |
| 28 | + 注意,你可以重复使用字典中的单词。 |
| 29 | +``` |
| 30 | +示例 3: |
| 31 | +``` |
| 32 | +输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] |
| 33 | +
|
| 34 | +输出: false |
| 35 | +``` |
| 36 | + |
| 37 | +提示: |
| 38 | +* $1 <= s.length <= 300$ |
| 39 | +* $1 <= wordDict.length <= 1000$ |
| 40 | +* $1 <= wordDict[i].length <= 20$ |
| 41 | +* `s` 和 `wordDict[i]` 仅有小写英文字母组成 |
| 42 | +* `wordDict` 中的所有字符串 互不相同 |
| 43 | + |
| 44 | +--- |
| 45 | + |
| 46 | +### 序列 DP |
| 47 | + |
| 48 | +将字符串 `s` 长度记为 $n$,`wordDict` 长度记为 $m$。为了方便,我们调整字符串 `s` 以及将要用到的动规数组的下标从 $1$ 开始。 |
| 49 | + |
| 50 | +定义 $f[i]$ 为考虑前 $i$ 个字符,能否使用 `wordDict` 拼凑出来:当 $f[i] = true$ 代表 $s[1...i]$ 能够使用 `wordDict` 所拼凑,反之则不能。 |
| 51 | + |
| 52 | +不失一般性考虑 $f[i]$ 该如何转移:由于 $f[i]$ 需要考虑 $s[1...i]$ 范围内的字符,若 $f[i]$ 为 `True` 说明整个 $s[1...i]$ 都能够使用 `wordDict` 拼凑,自然也包括最后一个字符 $s[i]$ 所在字符串 `sub`。 |
| 53 | + |
| 54 | +**我们可以枚举最后一个字符所在字符串的左端点 $j$,若 $sub = s[j...i]$ 在 `wordDict` 中出现过,并且 $f[j - 1] = True$,说明 $s[0...(j - 1)]$ 能够被拼凑,并且子串 `sub` 也在 `wordDict`,可得 `f[i] = True`。** |
| 55 | + |
| 56 | +为了快速判断某个字符是否在 `wordDict` 中出现,我们可以使用 `Set` 结构对 $wordDict[i]$ 进行转存。 |
| 57 | + |
| 58 | +Java 代码: |
| 59 | +```Java |
| 60 | +class Solution { |
| 61 | + public boolean wordBreak(String s, List<String> wordDict) { |
| 62 | + Set<String> set = new HashSet<>(); |
| 63 | + for (String word : wordDict) set.add(word); |
| 64 | + int n = s.length(); |
| 65 | + boolean[] f = new boolean[n + 10]; |
| 66 | + f[0] = true; |
| 67 | + for (int i = 1; i <= n; i++) { |
| 68 | + for (int j = 1; j <= i && !f[i]; j++) { |
| 69 | + String sub = s.substring(j - 1, i); |
| 70 | + if (set.contains(sub)) f[i] = f[j - 1]; |
| 71 | + } |
| 72 | + } |
| 73 | + return f[n]; |
| 74 | + } |
| 75 | +} |
| 76 | +``` |
| 77 | +TypeScript 代码: |
| 78 | +```TypeScript |
| 79 | +function wordBreak(s: string, wordDict: string[]): boolean { |
| 80 | + const ss = new Set<string>(wordDict) |
| 81 | + const n = s.length |
| 82 | + const f = new Array<boolean>(n + 10).fill(false) |
| 83 | + f[0] = true |
| 84 | + for (let i = 1; i <= n; i++) { |
| 85 | + for (let j = i; j >= 1 && !f[i]; j--) { |
| 86 | + const sub = s.substring(j - 1, i) |
| 87 | + if (ss.has(sub)) f[i] = f[j - 1] |
| 88 | + } |
| 89 | + } |
| 90 | + return f[n] |
| 91 | +} |
| 92 | +``` |
| 93 | +Python 代码: |
| 94 | +```Python |
| 95 | +class Solution: |
| 96 | + def wordBreak(self, s: str, wordDict: List[str]) -> bool: |
| 97 | + ss = set(wordDict) |
| 98 | + n = len(s) |
| 99 | + f = [False] * (n + 10) |
| 100 | + f[0] = True |
| 101 | + for i in range(1, n + 1): |
| 102 | + j = i |
| 103 | + while j >= 1 and not f[i]: |
| 104 | + sub = s[j - 1:i] |
| 105 | + if sub in ss: |
| 106 | + f[i] = f[j - 1] |
| 107 | + j -= 1 |
| 108 | + return f[n] |
| 109 | +``` |
| 110 | +* 时间复杂度:将 `wordDict` 转存在 `Set` 复杂度为 $O(m)$;`DP` 过程复忽裁剪子串和查询 `Set` 结构的常数,复杂度为 $O(n^2)$ |
| 111 | +* 空间复杂度:$O(n + m)$ |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +### 总结 |
| 116 | + |
| 117 | +这里简单说下「线性 DP」和「序列 DP」的区别。 |
| 118 | + |
| 119 | +线性 DP 通常强调「状态转移所依赖的前驱状态」是由给定数组所提供的,即拓扑序是由原数组直接给出。更大白话来说就是通常有 $f[i][...]$ 依赖于 $f[i - 1][...]$。 |
| 120 | + |
| 121 | +这就限定了线性 DP 的复杂度是简单由「状态数量(或者说是维度数)」所决定。 |
| 122 | + |
| 123 | +序列 DP 通常需要结合题意来寻找前驱状态,即需要自身寻找拓扑序关系(例如本题,需要自己通过枚举的方式来找左端点,从而找到可转移的前驱状态 $f[j - 1]$)。 |
| 124 | + |
| 125 | +这就限定了序列 DP 的复杂度是由「状态数 + 找前驱」的复杂度所共同决定。也直接导致了序列 DP 有很多玩法,往往能够结合其他知识点出题,来优化找前驱这一操作,通常是利用某些性质,或是利用数据结构进行优化。 |
| 126 | + |
| 127 | +--- |
| 128 | + |
| 129 | +### 最后 |
| 130 | + |
| 131 | +这是我们「刷穿 LeetCode」系列文章的第 `No.139` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 132 | + |
| 133 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 134 | + |
| 135 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 136 | + |
| 137 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 138 | + |
0 commit comments