LeetCode 139中等字符串 DP
单词拆分 图解题解
这道题到底在问什么
给字符串 s 和词典 wordDict,判断 s 是否能由词典中的词拼接而成。
- s
- "leetcode"
- wordDict
- ["leet","code"]
- 输出
- true
先想最直接的笨办法
贪心「每次拿最长」会在 "aaa" 后被单个 "a" 卡死、误判无解;DP 枚举所有分割点,找到 aa+aa → true。这就是必须用 DP、不能贪心的原因。(动画第 16 步)
最优解:一步一步想明白
- 3每个位置都能切,后缀会被反复判断。DP 把“前缀能否拆”记下来。
- 4如果存在一个切点 j,左边 s[0:j] 能拆,右边 s[j:i] 是词,那么 dp[i] 为 true。
- 5dp0空串可以被拆成 0 个词,所以 dp[0]=true。
- 6leet到 i=4 时,切点 j=0:dp[0]=true,s[0:4]=leet 在词典里。
- 7false到 i=5、6、7,找不到一个合法切点,所以保持 false。
- 8true到 i=8,切点 j=4:dp[4]=true,s[4:8]=code 在词典里。
- 9answer最后 dp[8]=true,说明整个字符串可以拆完。
- 10false case如果最后一个位置始终找不到合法切点,dp[n] 就是 false。
- 13切点 j 是这题的核心变量。
- 16贪心 aaa 后卡在单个 a;DP 发现 aa+aa 才是解贪心「每次拿最长」会在 "aaa" 后被单个 "a" 卡死、误判无解;DP 枚举所有分割点,找到 aa+aa → true。这就是必须用 DP、不能贪心的原因。
- 23这题学完不要乱跳,下一步去 对应专题 连刷同类题;卡住时用 AI 答疑追问“为什么这一步成立”,比只背答案更稳。
⚠️ 容易写错的地方
✗ 错:忘记 dp[0]=true
✓ 对:空前缀是合法起点
第一个词需要从空前缀转移。
✗ 错:只贪心匹配最长词
✓ 对:枚举所有切点
局部最长不一定能让后面拆完。
✗ 错:词典用 list 反复查
✓ 对:转成 set
查词要尽量 O(1)。
完整代码(Python / C++ / Java)
Python
class Solution:
def wordBreak(self, s, wordDict):
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]C++
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> words(wordDict.begin(), wordDict.end());
vector<bool> dp(s.size() + 1);
dp[0] = true;
for (int i = 1; i <= s.size(); i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && words.count(s.substr(j, i - j))) {
dp[i] = true;
break;
}
}
}
return dp.back();
}
};Java
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> words = new HashSet<>(wordDict);
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for (int i = 1; i <= s.length(); i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && words.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}套路模板 · 单词拆分迁移骨架
# 1. 说清 dp[状态] 的含义
dp = 初始值
for state in 遍历顺序:
for choice in 可选动作:
candidate = dp[前置状态] + 这次选择的代价
dp[state] = best(dp[state], candidate)
return dp[答案状态]// 1. 先定义 dp[state] 的含义
vector<int> dp(状态数, 初始值);
for (auto state : 遍历顺序) {
for (auto choice : 可选动作) {
auto candidate = dp[前置状态] + 代价;
dp[state] = best(dp[state], candidate);
}
}
return dp[答案状态];// 1. 先定义 dp[state] 的含义
int[] dp = new int[状态数];
Arrays.fill(dp, 初始值);
for (State state : 遍历顺序) {
for (Choice choice : 可选动作) {
int candidate = dp[前置状态] + 代价;
dp[state] = best(dp[state], candidate);
}
}
return dp[答案状态];复杂度
时间复杂度
O(n³)
枚举分割点 O(n²),每次取子串+查词典,子串最长 O(n),故最坏 O(n³)
空间复杂度
O(n + dict)
dp 数组和词典 set
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 单词拆分 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
如何返回所有拆分句子?+
LC140,需要 DFS + 记忆化,记录从每个位置出发的所有句子。
这道题为什么用「DP」,换最直接的暴力解会差在哪?+
DP抓住了本题的结构特征,把暴力解里重复的工作省掉;暴力解通常要多嵌套一层枚举,数据一大就超时。具体对比见上文「暴力解及其卡点」与「最优解逐步推演」两节。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。