LeetCode 140 单词拆分 II(输出所有可行拆分句子):python3 题解
题目链接:140. 单词拆分 II
140. 单词拆分 II - 完整题解
1. 题目理解
题目大意:
给你一个字符串 s 和一个单词字典 wordDict。你需要在 s 中插入空格,把它变成一个个句子,要求句子里的每一个单词都必须存在于字典 wordDict 中。你需要返回所有可能的句子组合。
关键点:
- 所有可能:这意味着不能找到一个解就停止,必须找出所有合法的分割方式。
- 重复使用:字典里的单词可以重复使用。
- 顺序:返回的句子顺序任意。
示例分析:
输入:s = "catsanddog", wordDict = ["cat","cats","and","sand","dog"]
- 第一种分法:
"cats"+"and"+"dog"->"cats and dog" - 第二种分法:
"cat"+"sand"+"dog"->"cat sand dog" - 输出:
["cats and dog", "cat sand dog"]
2. 解题思路分析
这道题是经典的搜索 + 记忆化问题。我们可以把它想象成“切绳子”:我们要把字符串 s 切成若干段,每一段都必须是字典里的单词。
思路一:回溯法 (Backtracking / DFS)
最直观的想法是从字符串的开头开始,尝试截取不同长度的前缀。
- 如果前缀在字典里,就把它当作第一个单词,然后对剩下的字符串递归进行同样的操作。
- 如果剩下的字符串也能被完全分割,那么我们就找到了一个合法的句子。
- 缺点:如果不加优化,会有大量的重复计算。例如字符串 "aaaa...",每次递归都会重新计算后面部分的可能性。
思路二:记忆化搜索 (Memoization) —— 推荐解法
在回溯法的基础上,我们增加一个“缓存”(字典或数组)。
- 当我们计算过
s[i:](从索引i到末尾的子串)的所有可能句子后,把结果存起来。 - 下次如果再遇到需要计算
s[i:]的情况,直接从缓存里取结果,不用再递归。 - 优点:避免了重复子问题的计算,效率大幅提升。
思路三:动态规划 (Dynamic Programming)
自底向上构建解。
- 定义
dp[i]为子串s[i:]能构成的所有句子列表。 - 从后往前遍历,如果
s[i:j]是单词,且dp[j]有解,则将s[i:j]与dp[j]中的句子组合。 - 优点:逻辑清晰,无递归栈溢出风险。
- 缺点:代码实现上处理字符串拼接稍显繁琐,通常记忆化搜索写起来更简洁。
高级优化:可行性剪枝
在开始生成所有句子之前,先利用 单词拆分 I (Word Break I) 的逻辑(布尔型 DP)判断一下 s 是否至少能被拆分。
- 如果根本不能拆分,直接返回空列表
[]。 - 这可以防止在某些极端测试用例(字符串很长但无解)下超时。
3. 代码实现 (记忆化搜索)
下面提供基于 记忆化搜索 (DFS + Memo) 的 Python 3 代码。这是最通用、代码最清晰且效率较高的解法。
from typing import List
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
"""
主函数:接收字符串 s 和单词列表 wordDict,返回所有可能的句子
"""
# 1. 预处理:将 wordDict 转换为集合 (set)
# 原因:集合的查找时间复杂度是 O(1),而列表是 O(N),能显著加快判断速度
word_set = set(wordDict)
# 2. 创建记忆化缓存字典
# key: 起始索引 index
# value: 从该索引开始到字符串末尾,所有可能的句子列表
memo = {}
def dfs(start: int) -> List[str]:
"""
深度优先搜索辅助函数
参数 start: 当前处理到的字符串 s 的索引位置
返回:s[start:] 部分所有可能的句子组合列表
"""
# --- 终止条件 ---
# 如果 start 达到了字符串长度,说明前面已经完美匹配完了
# 返回一个包含空字符串的列表,方便上层进行拼接
if start == len(s):
return [""]
# --- 记忆化检查 ---
# 如果这个起始位置已经计算过,直接返回缓存的结果
if start in memo:
return memo[start]
# 存储当前起始位置的所有有效句子
sentences = []
# --- 尝试所有可能的分割点 ---
# end 从 start + 1 遍历到 len(s),表示截取 s[start:end]
for end in range(start + 1, len(s) + 1):
word = s[start:end]
# 如果截取的单词在字典中
if word in word_set:
# 递归计算剩余部分 (s[end:]) 的所有句子
rest_sentences = dfs(end)
# 将当前单词与剩余部分的句子组合
for rest in rest_sentences:
if rest == "":
# 如果剩余部分是空(说明刚好到末尾),直接添加当前单词
sentences.append(word)
else:
# 否则,当前单词 + 空格 + 剩余句子
sentences.append(word + " " + rest)
# --- 存入缓存并返回 ---
memo[start] = sentences
return sentences
# 从索引 0 开始搜索
return dfs(0)
4. 代码详解与逻辑图解
为了让你更明白代码是如何运行的,我们以示例 s = "catsanddog", wordDict = ["cat","cats","and","sand","dog"] 为例。
- 初始调用
dfs(0)。 - 第一轮循环 (
start=0):- 截取
"c"(不在字典) - 截取
"ca"(不在字典) - 截取
"cat"(在字典) -> 递归调用dfs(3)(处理 "sanddog") - 截取
"cats"(在字典) -> 递归调用dfs(4)(处理 "anddog") - ...
- 截取
- 递归深入 (
dfs(3)处理 "sanddog"):- 尝试
"s","sa","san"... - 截取
"sand"(在字典) -> 递归调用dfs(7)(处理 "dog")
- 尝试
- 递归深入 (
dfs(7)处理 "dog"):- 截取
"dog"(在字典) -> 递归调用dfs(10)
- 截取
- 触底 (
dfs(10)):start == len(s),返回[""]。
- 回溯组合:
dfs(7)收到[""],组合"dog" + " " + ""-> 得到["dog"],存入memo[7]。dfs(3)收到["dog"],组合"sand" + " " + "dog"-> 得到["sand dog"],存入memo[3]。dfs(0)收到["sand dog"],组合"cat" + " " + "sand dog"-> 得到["cat sand dog"]。
- 同理,另一条路径
"cats"会生成"cats and dog"。 - 最终
dfs(0)返回["cat sand dog", "cats and dog"]。
5. 复杂度分析
假设字符串 s 的长度为 \(N\),字典中单词的最大长度为 \(L\)。
- 时间复杂度: \(O(2^N \cdot N)\) 在最坏情况下(例如
s="aaaaa",dict=["a"]),每个字符都可以选择切或不切。但在加入记忆化后,每个子问题dfs(i)只计算一次。对于每个i,我们需要遍历最多 \(L\) 个长度的单词,并且需要拼接字符串。实际运行效率远优于纯指数级,通常可以视为 \(O(N \cdot 2^N)\) 或更优,取决于解的数量。对于本题 \(N \le 20\),完全可以通过。 - 空间复杂度: \(O(2^N \cdot N)\)。主要用于存储记忆化缓存
memo和递归调用栈。最坏情况下,所有可能的句子都需要存储。
6. 进阶优化:可行性剪枝 (可选)
虽然上面的代码对于 \(N \le 20\) 已经足够快,但在某些更严格的限制下(例如 \(N\) 更大),如果字符串根本无法拆分,记忆化搜索仍然会尝试所有组合直到最后才发现不行。
我们可以先运行一次 单词拆分 I 的逻辑(布尔 DP),如果返回 False,直接返回 []。
# 可以在 wordBreak 函数开头加入这段优化代码
def can_break(s, word_set):
n = len(s)
dp = [False] * (n + 1)
dp[0] = True
for i in range(1, n + 1):
for j in range(i):
if dp[j] and s[j:i] in word_set:
dp[i] = True
break
return dp[n]
# 使用示例:
# if not can_break(s, word_set):
# return []
注:对于本题给定的数据范围(长度 20),这个优化不是必须的,但这是一个很好的工程化思维。
7. 总结
- 核心算法:使用 DFS (深度优先搜索) 遍历所有分割可能性。
- 关键优化:使用 Memoization (记忆化) 缓存
dfs(index)的结果,避免重复计算子串。 - 数据结构:将
wordDict转为set以加速查找。 - 字符串拼接:注意处理空格,当递归返回空列表表示到达末尾时,不要多加空格。

浙公网安备 33010602011771号