全栈开发知识库

110.单词拆分

题目

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 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[i] 表示字符串 s 的前 i 个字符(即 s[0..i-1])能否被成功拆分。

初始状态:dp[0] = true,表示空字符串是可以拆分的(不取任何单词)。

状态转移:对于每个 i(从 1 到 s.length()),我们尝试找一个分割点 j(0 <= j < i),使得: - dp[j] = true(前 j 个字符可以被拆分) - 并且子串 s[j..i-1](即从第 j 个字符到第 i-1 个字符)在字典中 如果存在这样的 j,那么 dp[i] = true。

最终答案:dp[s.length()]。

举个例子:s = "leetcode", dict = ["leet","code"] - 初始化 dp[0] = true。 - i = 1(检查前1个字符 "l"):没有 j 使得子串在字典中 → dp[1] = false - i = 2 ("le"):false - i = 3 ("lee"):false - i = 4 ("leet"):j = 0 时,dp[0]=true,子串 s[0..3]="leet" 在字典中 → dp[4] = true - i = 5 ("leetc"):尝试 j = 4,dp[4]=true,子串 s[4..4]="c" 不在字典中;其他 j 都不行 → dp[5]=false - i = 6 ("leetco"):尝试 j = 4,子串 s[4..5]="co" 不在字典中 → false - i = 7 ("leetcod"):尝试 j = 4,子串 s[4..6]="cod" 不在字典中 → false - i = 8 ("leetcode"):尝试 j = 4,子串 s[4..7]="code" 在字典中,且 dp[4]=true → dp[8]=true 所以最终 dp[8]=true,可以拆分。

优化技巧

  • 用 HashSet 存储字典:方便 O(1) 判断子串是否在字典中。
  • 限制子串长度:我们可以先找出字典中最长单词的长度 maxLen,那么在检查子串时,子串长度不能超过 maxLen,这样- 可以减少不必要的循环。即 j 从 i - maxLen 到 i-1(且 j >= 0)。
  • 剪枝:如果 dp[i] 已经为 true,可以提前跳出内层循环。
class Solution {
    fun wordBreak(s: String, wordDict: List<String>): Boolean {
        // 把字典放进集合里,查找一个单词在不在字典中会更快
        val set = wordDict.toHashSet()

        // dp[i] 表示:s 的前 i 个字符,能不能被拆分成功
        val dp = BooleanArray(s.length + 1)

        // 空字符串默认是可以拆分成功的
        dp[0] = true

        // i 表示前 i 个字符
        for (i in 1..s.length) {
            // 枚举最后一刀切在哪里
            for (j in 0 until i) {
                // 如果前 j 个字符已经能拆成功,
                // 并且 s[j, i) 这一段正好是字典里的单词,
                // 那么前 i 个字符也能拆成功
                if (dp[j] && s.substring(j, i) in set) {
                    dp[i] = true
                    break // 跳出j循环,继续判断下一个i
                }
            }
        }

        return dp[s.length]  # 最后一个字符都判断完,如果能切就可以
    }
}

复杂度

  • 时间复杂度: O(n² × L) 其中 n 是 s 的长度,L 是子串截取和哈希查找的平均时间(通常视为 O(1)),所以实际上是 O(n²)。加上最大长度优化后,平均复杂度降低。
  • 空间复杂度: 空间复杂度:O(n + m),n 是 dp 数组长度,m 是字典大小。


  • 邮箱 :charon.chui@gmail.com
  • Good Luck!
本页由 Markdown 自动渲染 · 共 628 篇笔记