在问 · 算法题解

139. 单词拆分

题目描述

输入字符串 s 和字符串列表 wordDict(字典)。

判断能否把 s 完整地切成一段或多段,使每一段都是字典里的单词。字典里的单词可以反复使用,也不必全部用到。

能切则返回 true,否则返回 false。

示例 1
输入:s = "leetcode", wordDict = ["leet","code"]
输出:true

解释:切成 leet 和 code。

示例 2
输入:s = "applepenapple", wordDict = ["apple","pen"]
输出:true

解释:切成 apple、pen、apple,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 中没有重复单词。

解题思路

dp[i] 表示前 i 个字符能否拆分,若存在 j 使 dp[j] 且 s[j:i] 在字典中则成立。

为什么正确

dp[i] 确认前 i 个字符完整可拆分,不依赖未处理的后缀。

复杂度分析

Python 子串切片和哈希计入后,最坏时间 O(n³);辅助空间 O(n+D),D 为字典集合大小。

易错点

不能只按字符切分;每次转移要同时保证前缀可达和当前子串在字典中。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

from typing import List

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        words = set(wordDict)
        dp = [False] * (len(s) + 1)
        dp[0] = True
        for i in range(1, len(s) + 1):
            for j in range(i):
                if dp[j] and s[j:i] in words:
                    dp[i] = True
                    break
        return dp[-1]

载入编辑器,自己跑一遍 →

其他解法

自底向上的动态规划枚举切分点;记忆化搜索自顶向下枚举单词,只计算可达的状态。k 为单词数,L 为最长单词长度。

解法 2:记忆化搜索 时间 O(n·k·L),空间 O(n)

can(i) 表示从下标 i 开始的后缀能否拆分。枚举字典里的每个单词,若它是 s 在下标 i 处的前缀,就递归判断 can(i + len(w))。每个下标的结果只算一次并缓存。

按单词枚举,字典小、单词短时很快,并且只访问能到达的下标。递归深度最多为 n。

from functools import lru_cache
from typing import List

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        n = len(s)

        @lru_cache(None)
        def can(i):
            if i == n:
                return True
            for w in wordDict:
                if s.startswith(w, i) and can(i + len(w)):
                    return True
            return False

        return can(0)

动态规划 专题的其他题目

查看 动态规划 全部题目 →