1. 项目概述:Word Break问题与力扣算法解析
Word Break(单词拆分)是力扣平台上经典的动态规划题型,编号为第139题。这道题要求判断给定字符串能否被拆分成字典中的单词组合,考察的是对字符串处理、边界条件分析和动态规划状态转移的综合应用能力。
我在刷题过程中发现,很多初学者容易陷入暴力递归的陷阱,或者无法准确构建状态转移方程。本文将分享一套经过实战检验的解题框架,包含Python和Java两种实现,并重点解析如何将问题转化为标准的动态规划模型。对于正在准备算法面试的开发者,掌握这类分割型问题的解题模式能显著提升笔试通过率。
2. 核心算法原理与解题思路
2.1 问题重述与示例分析
给定一个非空字符串s和一个包含非空单词列表的字典wordDict,判断s是否可以被分割成一个或多个字典单词的空格分隔序列。例如:
- 输入:s = "leetcode", wordDict = ["leet", "code"]
- 输出:true("leetcode"可拆分为"leet code")
2.2 动态规划解法设计
正确的解法应采用动态规划,其核心在于:
- 定义dp数组:dp[i]表示字符串s的前i个字符能否被拆分
- 初始化:dp[0] = true(空字符串视为可拆分)
- 状态转移方程:
for j in range(i): if dp[j] and s[j:i] in wordDict: dp[i] = True break - 最终结果:dp[n](n为字符串长度)
2.3 时间复杂度优化
将wordDict转换为集合(set)可将查找操作降至O(1):
wordSet = set(wordDict) max_len = max(len(word) for word in wordSet) if wordSet else 0加入最大长度剪枝:
for j in range(i-1, max(i-max_len-1, -1), -1): if dp[j] and s[j:i] in wordSet: dp[i] = True break这样可将时间复杂度从O(n³)优化至O(n²)
3. 完整代码实现与逐行解析
3.1 Python版本实现
def wordBreak(s, wordDict): wordSet = set(wordDict) max_len = max(len(word) for word in wordSet) if wordSet else 0 n = len(s) dp = [False] * (n + 1) dp[0] = True for i in range(1, n + 1): start = max(0, i - max_len - 1) for j in range(start, i): if dp[j] and s[j:i] in wordSet: dp[i] = True break return dp[n]关键点说明:
- 第2行:转换为集合提升查找效率
- 第3行:记录字典中最长单词长度
- 第6行:dp数组初始化
- 第9行:通过max_len限制内层循环范围
- 第11行:核心状态转移判断
3.2 Java版本实现
public boolean wordBreak(String s, List<String> wordDict) { Set<String> wordSet = new HashSet<>(wordDict); int maxLen = wordSet.stream().mapToInt(String::length).max().orElse(0); boolean[] dp = new boolean[s.length() + 1]; dp[0] = true; for (int i = 1; i <= s.length(); i++) { int start = Math.max(0, i - maxLen - 1); for (int j = start; j < i; j++) { if (dp[j] && wordSet.contains(s.substring(j, i))) { dp[i] = true; break; } } } return dp[s.length()]; }4. 常见错误分析与调试技巧
4.1 典型错误案例
直接使用递归导致超时:
# 错误示范(指数级时间复杂度) def wordBreak(s, wordDict): if not s: return True return any(s.startswith(word) and wordBreak(s[len(word):], wordDict) for word in wordDict)忘记处理空字符串初始化:
// 错误示范:未初始化dp[0] boolean[] dp = new boolean[s.length() + 1]; // 直接开始循环会导致错误
4.2 调试建议
打印dp数组观察状态变化:
print(f"i={i}, dp={dp}")边界条件测试用例:
- 空字符串
- 字典为空
- 字符串包含字典中没有的字符
- 字典中有空字符串
性能测试用例:
s = "a"*100 + "b" wordDict = ["a"] + ["a"*i for i in range(1, 10)]
5. 算法扩展与变种问题
5.1 Word Break II(输出所有可能分割)
在判断能否分割的基础上,需要记录所有可能的分割方式。此时需要:
- 使用记忆化递归存储中间结果
- 回溯法构建所有路径
5.2 前缀树(Trie)优化
当字典规模极大时,可用前缀树替代集合:
class TrieNode: def __init__(self): self.children = {} self.is_word = False def build_trie(wordDict): root = TrieNode() for word in wordDict: node = root for c in word: node = node.children.setdefault(c, TrieNode()) node.is_word = True return root5.3 实际应用场景
- 文本断词处理
- 代码语法检查
- 生物信息学的序列分析
- 密码破解中的字典攻击
6. 刷题经验与面试技巧
识别动态规划问题的特征:
- 求"是否可行"、"最值"、"总数"类问题
- 问题可以分解为子问题
- 子问题之间存在重叠
面试应答策略:
- 先说明暴力解法及其缺陷
- 引出动态规划优化思路
- 讨论时间/空间复杂度
- 提出可能的优化方向
力扣刷题路线建议:
- 先掌握基础动态规划(斐波那契、爬楼梯)
- 再练习线性DP(最大子数组和、买卖股票)
- 最后攻克字符串DP(编辑距离、回文分割)
提示:在面试中遇到变种问题时,可以先询问面试官是否允许使用字典预处理等优化手段,这既能展示沟通能力,也体现工程思维。