📌 题目链接:139. 单词拆分 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:字符串、动态规划、哈希表
⏱️ 目标时间复杂度:O(n²)
💾 空间复杂度:O(n)
在面试和算法竞赛中, “单词拆分” 是一道经典的 字符串 + 动态规划(DP) 综合题。它不仅考察你对 DP 状态设计的理解,还涉及 字符串子串提取效率 与 字典查找优化 的工程思维。
本题要求判断一个字符串是否能被字典中的单词 完全拼接而成(可重复使用),这正是典型的 “完全背包”思想在字符串上的体现。
🔍 题目分析
给定:
- 字符串
s(长度 ≤ 300) - 字典
wordDict(最多 1000 个互不相同的单词,每个单词长度 ≤ 20)
目标:判断 s 是否能由 wordDict 中的单词 按顺序拼接而成(不要求用完所有单词,但必须覆盖整个 s)。
关键点:
- ✅ 单词可重复使用 → 无限次选择 → 类似完全背包
- ✅ 必须从左到右连续拼接 → 不能跳过字符
- ❌ 不能打乱顺序或插入额外字符
💡 面试高频考点:此题常被用于考察候选人是否能将“字符串分割”问题转化为 DP 状态转移模型,并意识到 暴力递归会超时,从而引入 记忆化或 DP 优化。
⚙️ 核心算法及代码讲解
🎯 核心思想:动态规划(Dynamic Programming)
我们定义状态:
dp[i]表示 字符串s的前i个字符(即s[0..i-1])是否可以被字典中的单词拼接而成。
-
初始状态:
dp[0] = true(空字符串总是合法的) -
状态转移:对于每个位置
i(1 ≤ i ≤ n),我们尝试所有可能的分割点j(0 ≤ j < i):- 如果
dp[j] == true(前 j 个字符可拼接) - 且子串
s.substr(j, i - j)在字典中存在 - 那么
dp[i] = true
- 如果
📌 为什么是
s.substr(j, i - j)?
因为substr(pos, len)在 C++ 中是从pos开始取len个字符,而j到i-1共有i - j个字符。
为了快速判断子串是否在字典中,我们将 wordDict 转为 unordered_set(哈希集合) ,实现 O(1) 平均查找时间。
💻 C++ 核心算法代码(带逐行注释)
// 将 wordDict 转为哈希集合,加速查找
auto wordDictSet = unordered_set<string>();
for (auto word : wordDict) {
wordDictSet.insert(word);
}
// dp[i]:s 的前 i 个字符能否被拆分
auto dp = vector<bool>(s.size() + 1);
dp[0] = true; // 空串合法
// 枚举终点 i(1 到 n)
for (int i = 1; i <= s.size(); ++i) {
// 枚举分割点 j(0 到 i-1)
for (int j = 0; j < i; ++j) {
// 若前 j 个字符可拆分,且 s[j..i-1] 在字典中
if (dp[j] && wordDictSet.find(s.substr(j, i - j)) != wordDictSet.end()) {
dp[i] = true;
break; // 找到一种合法拆分即可,无需继续
}
}
}
✅ 关键优化:内层循环一旦找到合法
j就break,避免无效计算。
🧩 解题思路(分步详解)
- 预处理字典
将wordDict存入unordered_set,使后续子串查找为 O(1)。 - 初始化 DP 数组
dp[0] = true是边界条件,表示“空字符串可被拆分”。 - 外层循环:遍历字符串终点
i
从 1 到n,依次判断s[0..i-1]是否可拆分。 - 内层循环:枚举所有可能的最后一个单词起点
j
对于每个i,尝试所有j ∈ [0, i),检查s[j..i-1]是否是一个字典单词。 - 状态转移
若dp[j]为真(前面部分可拆分)且s[j..i-1]在字典中,则dp[i] = true。 - 提前终止
一旦找到合法j,立即break,因为只需判断“是否存在”,而非“有多少种”。 - 返回结果
最终答案为dp[n],即整个字符串是否可拆分。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n²):两层循环,每层最多 n 次;每次 substr 最坏 O(n),但实际平均较短(因单词长度 ≤ 20)。 ⚠️ 严格来说,若考虑 substr 复制开销,最坏为 O(n³),但题目限制单词长度 ≤ 20,可视为常数,故通常记为 O(n²) 。 |
| 空间复杂度 | O(n + m):dp 数组 O(n),哈希集合存储字典 O(m),其中 m 为字典总字符数。 |
| 是否可优化? | ✅ 可通过 Trie(字典树) 避免 substr 复制,直接从 j 向后匹配,将内层匹配变为 O(L)(L 为最大单词长),总复杂度仍 O(n·L),更优。但本题 n 较小,哈希表已足够。 |
💬 面试加分点:
“除了 DP + 哈希表,还可以用 BFS 或 DFS + 记忆化 解决。BFS 把每个可到达的位置加入队列,避免重复计算;记忆化 DFS 从起点出发,尝试所有可能的单词匹配。但 DP 是最直观且高效的解法。”
💻 完整代码
✅ C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
auto wordDictSet = unordered_set<string>();
for (auto word : wordDict) {
wordDictSet.insert(word);
}
auto dp = vector<bool>(s.size() + 1);
dp[0] = true;
for (int i = 1; i <= s.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (dp[j] && wordDictSet.find(s.substr(j, i - j)) != wordDictSet.end()) {
dp[i] = true;
break;
}
}
}
return dp[s.size()];
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 示例 1
string s1 = "leetcode";
vector<string> dict1 = {"leet", "code"};
cout << sol.wordBreak(s1, dict1) << "\n"; // 输出: 1
// 示例 2
string s2 = "applepenapple";
vector<string> dict2 = {"apple", "pen"};
cout << sol.wordBreak(s2, dict2) << "\n"; // 输出: 1
// 示例 3
string s3 = "catsandog";
vector<string> dict3 = {"cats", "dog", "sand", "and", "cat"};
cout << sol.wordBreak(s3, dict3) << "\n"; // 输出: 0
return 0;
}
✅ JavaScript
/**
* @param {string} s
* @param {string[]} wordDict
* @return {boolean}
*/
var wordBreak = function(s, wordDict) {
const wordSet = new Set(wordDict);
const n = s.length;
const dp = new Array(n + 1).fill(false);
dp[0] = true;
for (let i = 1; i <= n; i++) {
for (let j = 0; j < i; j++) {
if (dp[j] && wordSet.has(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[n];
};
// 测试
console.log(wordBreak("leetcode", ["leet", "code"])); // true
console.log(wordBreak("applepenapple", ["apple", "pen"])); // true
console.log(wordBreak("catsandog", ["cats", "dog", "sand", "and", "cat"])); // false
🌟 总结 & 面试要点
- ✅ DP 状态定义是关键:
dp[i]表示前缀是否可拆分。 - ✅ 哈希表优化字典查找:避免每次线性搜索
wordDict。 - ✅ 理解“完全背包”思想:单词可重复使用,类似物品可无限选。
- ✅ 注意
substr开销:在极端情况下可能成为瓶颈,可提 Trie 优化。 - ✅ 边界条件
dp[0]=true不可遗漏。
📌 高频变体题:
- 返回所有可能的拆分方案(需回溯)
- 字典中有通配符(如
.)- 单词不可重复使用(0-1 背包)
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!