单词拆分(Word Break)题解 | 动态规划解法
问题描述
给定一个非空字符串 s 和一个包含若干非空单词的字符串列表 wordDict(字典),判断是否可以将字符串 s 拆分为若干个字典中的单词(字典中的单词可重复使用,无需用完所有单词)。
核心特征分析
本题的核心是字符串拆分的可达性判断,核心特征可归纳为:
- 问题本质:判断整个字符串能否被划分为字典中单词的组合,属于"可达性"判定问题(而非求所有拆分方案);
- 重叠子问题:判断前
i个字符能否拆分时,会重复用到前j(j < i)个字符的拆分结果,若暴力计算会重复求解子问题; - 最优子结构:前
i个字符的拆分结果,可由"前j个字符可拆分"且"j到i之间的子串在字典中"两个条件推导得出。
算法选择
可选算法对比
- 暴力搜索/回溯:可枚举所有可能的拆分方式,但会重复计算大量子问题(如多次判断前
k个字符能否拆分),时间复杂度呈指数级,效率极低; - 动态规划(DP):通过缓存子问题的解(DP数组)避免重复计算,仅需线性空间存储状态,时间复杂度可优化至多项式级别。
最终选择
优先选择动态规划,原因:题目仅需判断"能否拆分"而非"所有拆分方案",DP可通过状态缓存高效解决重叠子问题,是此类可达性问题的最优选择。
解题模式识别(动态规划适用场景)
当遇到以下特征的字符串问题时,可优先考虑动态规划:
- 问题目标是"判断能否完成某类组合/分割",而非求所有解或最优解;
- 子问题的解可推导原问题的解,且存在明确的最优子结构;
- 字符串拆分/分割类问题(如分割回文串、单词拆分等),通常存在重叠子问题。
解题思路
动态规划的核心是定义状态并推导转移方程,具体步骤如下:
- 预处理字典 :将
wordDict转换为哈希集合(unordered_set),将子串查询的时间复杂度从O(m)(m为字典长度)降至O(1); - 定义DP状态 :设
dp[i]表示"字符串s的前i个字符(即s[0...i-1])能否被拆分为字典中的单词"; - 初始化状态 :
dp[0] = true(空字符串默认可拆分,作为递归/迭代的起始条件); - 状态转移 :
- 遍历字符串长度
i(从1到s的总长度n),逐一判断前i个字符的可达性; - 对每个
i,遍历分割点j(从0到i-1),若满足两个条件:dp[j] = true(前j个字符可拆分);- 子串
s[j...i-1](即s.substr(j, i-j))存在于字典中;
- 则说明前
i个字符可拆分,令dp[i] = true(找到一种可行方案即可,无需继续遍历j);
- 遍历字符串长度
- 返回结果 :最终
dp[n]即为整个字符串s的拆分结果(n是s的长度)。
解题代码(带详细注释)
cpp
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int n = s.size();
// 1. 字典转哈希集合,优化查询效率
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
// 2. 定义dp数组:dp[i]表示前i个字符能否被拆分
vector<bool> dp(n + 1, false);
// 3. 初始状态:空字符串可拆分
dp[0] = true;
// 4. 遍历所有可能的字符串长度i(前i个字符)
for (int i = 1; i <= n; ++i) {
// 遍历所有可能的分割点j
for (int j = 0; j < i; ++j) {
// 核心条件:前j个字符可拆分 + j到i的子串在字典中
if (dp[j] && wordSet.count(s.substr(j, i - j))) {
dp[i] = true;
break; // 找到一种方案即可,无需继续遍历j
}
}
}
// 5. 返回整个字符串的拆分结果
return dp[n];
}
};
复杂度分析
时间复杂度
O(n²),其中 n 是字符串 s 的长度:
- 外层循环遍历
i(1到n),共n次; - 内层循环遍历
j(0到i-1),最坏情况下总遍历次数为1+2+...+n = n(n+1)/2,近似为O(n²); - 哈希集合的查询操作是
O(1),子串截取s.substr(j, i-j)的时间复杂度为O(i-j),但由于字典中单词长度通常远小于n,实际时间复杂度接近O(n²)。
空间复杂度
O(n):
- 主要开销为
dp数组(长度n+1),哈希集合的空间取决于字典大小(题目未要求优化字典空间,属于输入相关开销)。
总结
- 本题核心是利用动态规划解决字符串拆分的可达性问题 ,通过
dp[i]缓存前i个字符的拆分状态,避免重复计算; - 关键优化点:将字典转为哈希集合,将子串查询效率从
O(m)降至O(1); - 状态转移的核心逻辑:
dp[i] = dp[j] && (s[j:i] ∈ 字典),找到任意一个可行的j即可确定dp[i] = true。