主题
题意
把
判断能否划分。
注意可以不划分,如果
一、寻找子问题
例如
- 长为
,即子串 ,如果它在 中,那么问题变成:能否把 划分成若干段,使得每段都在 中? - 长为
,即子串 ,如果它在 中,那么问题变成:能否把 划分成若干段,使得每段都在 中? - 长为
,即子串 ,如果它在 中,那么问题变成:能否把 划分成若干段,使得每段都在 中? - 长为
,即子串 ,如果它在 中,那么问题变成:能否把 划分成若干段,使得每段都在 中? - ……
这些问题都是和原问题相似的、规模更小的子问题,可以用递归解决。
⚠注意:本题
注:从右往左思考,主要是为了方便把递归翻译成递推。从左往右思考也是可以的。
二、状态定义与状态转移方程
根据上面的讨论,定义状态为
枚举
- 长为
,即子串 ,如果它在 中,那么问题变成:能否把前缀 划分成若干段,使得每段都在 中,即 。 - 长为
,即子串 ,如果它在 中,那么问题变成:能否把前缀 划分成若干段,使得每段都在 中,即 。 - 长为
,即子串 ,如果它在 中,那么问题变成:能否把前缀 划分成若干段,使得每段都在 中,即 。 - ……
设
枚举
用数学记号表示,就是
递归边界:
递归入口:
代码实现时,可以把
三、递归搜索 + 保存递归返回值 = 记忆化搜索
考虑到整个递归过程中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:
- 如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个
数组中。 - 如果一个状态不是第一次遇到(
中保存的结果不等于 的初始值),那么可以直接返回 中保存的结果。
注意:
Python 用户可以无视上面这段,直接用
@cache装饰器。
具体请看视频讲解 动态规划入门:从记忆化搜索到递推,其中包含把记忆化搜索 1:1 翻译成递推的技巧。
python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
max_len = max(map(len, wordDict)) # 用于限制下面 j 的循环次数
words = set(wordDict) # 便于快速判断 s[j:i] in words
@cache # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
def dfs(i: int) -> bool:
if i == 0: # 成功拆分!
return True
for j in range(i - 1, max(i - max_len - 1, -1), -1):
if s[j:i] in words and dfs(j):
return True
return False
return dfs(len(s))cpp
// C++ 版待补充python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
max_len = max(map(len, wordDict)) # 用于限制下面 j 的循环次数
words = set(wordDict) # 便于快速判断 s[j:i] in words
@cache # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
def dfs(i: int) -> bool:
if i == 0: # 成功拆分!
return True
return any(s[j:i] in words and dfs(j)
for j in range(i - 1, max(i - max_len - 1, -1), -1))
return dfs(len(s))cpp
// C++ 版待补充cpp
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int max_len = ranges::max(wordDict, {}, &string::size).size();
unordered_set<string> words(wordDict.begin(), wordDict.end());
int n = s.size();
vector<int> memo(n + 1, -1); // -1 表示没有计算过
auto dfs = [&](this auto&& dfs, int i) -> bool {
if (i == 0) { // 成功拆分!
return true;
}
int& res = memo[i]; // 注意这里是引用
if (res != -1) { // 之前计算过
return res;
}
for (int j = i - 1; j >= max(i - max_len, 0); j--) {
if (words.contains(s.substr(j, i - j)) && dfs(j)) {
return res = true; // 记忆化
}
}
return res = false; // 记忆化
};
return dfs(n);
}
};复杂度分析
- 时间复杂度:
,其中 是 的长度, 是 中字符串的最长长度, 是 的长度。创建哈希集合需要 的时间。由于每个状态只会计算一次,动态规划的时间复杂度 状态个数 单个状态的计算时间。本题状态个数等于 ,单个状态的计算时间为 (注意判断子串是否在哈希集合中需要 的时间),所以记忆化搜索的时间复杂度为 。 - 空间复杂度:
。哈希集合需要 的空间。记忆化搜索需要 的空间。
四、1:1 翻译成递推
我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。
具体来说,
同样地,枚举
用数学记号表示,就是
初始值
答案为
答疑
问:能不能外层循环枚举
答:不能。完全背包是同一个物品连续选择,然后就再也不选这个物品了。本题可以交替选。比如
python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
max_len = max(map(len, wordDict)) # 用于限制下面 j 的循环次数
words = set(wordDict) # 便于快速判断 s[j:i] in words
n = len(s)
f = [True] + [False] * n
for i in range(1, n + 1):
for j in range(i - 1, max(i - max_len - 1, -1), -1):
if f[j] and s[j:i] in words:
f[i] = True
break
return f[n]cpp
// C++ 版待补充python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
max_len = max(map(len, wordDict)) # 用于限制下面 j 的循环次数
words = set(wordDict) # 便于快速判断 s[j:i] in words
n = len(s)
f = [True] + [False] * n
for i in range(1, n + 1):
f[i] = any(f[j] and s[j:i] in words
for j in range(i - 1, max(i - max_len - 1, -1), -1))
return f[n]cpp
// C++ 版待补充cpp
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int max_len = ranges::max(wordDict, {}, &string::size).size();
unordered_set<string> words(wordDict.begin(), wordDict.end());
int n = s.size();
vector<int> f(n + 1);
f[0] = true;
for (int i = 1; i <= n; i++) {
for (int j = i - 1; j >= max(i - max_len, 0); j--) {
if (f[j] && words.contains(s.substr(j, i - j))) {
f[i] = true;
break;
}
}
}
return f[n];
}
};复杂度分析
- 时间复杂度:
,其中 是 的长度, 是 中字符串的最长长度, 是 的长度。理由同上。 - 空间复杂度:
。
注:本题字符串比较短,可以直接用哈希集合判断。如果字符串比较长,可以用字典树优化到
专题训练
见下面动态规划题单的「五、划分型 DP」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府