📌 题目链接:131. 分割回文串 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:回溯算法、动态规划、字符串处理
⏱️ 目标时间复杂度:O(n ⋅ 2ⁿ) (最坏情况)
💾 空间复杂度:O(n²) (用于预处理回文信息)
在 LeetCode Hot100 中,第 131 题「分割回文串」 是一道经典的 组合型回溯问题,同时巧妙融合了 动态规划预处理 的思想。它不仅考察你对递归与回溯的理解,还要求你具备优化重复计算的意识——这正是面试官最爱问的“如何避免暴力中的冗余? ”的经典场景。
本文将带你从 题目本质 → 核心算法 → 解题步骤 → 复杂度分析 → 多语言实现 全链路打通,确保你不仅能 AC 这道题,还能在面试中清晰表达设计思路!
🔍 题目分析
给定一个字符串 s,要求将其 分割成若干子串,使得 每个子串都是回文串,并返回 所有可能的分割方案。
-
✅ 输入:字符串
s(长度 1~16,仅小写字母) -
✅ 输出:二维字符串数组,每行是一种合法分割
-
✅ 关键点:
- 所有子串必须是回文;
- 要求 所有可能方案 → 枚举问题 → 回溯(Backtracking)
- 判断回文若每次都用双指针,会重复计算 → 需预处理
💡 为什么是回溯?
因为我们要“尝试所有可能的切分位置”,并在满足条件时记录路径,不满足则撤销选择——这正是回溯的典型特征:决策树遍历 + 状态恢复。
⚙️ 核心算法及代码讲解
本题的核心在于 两个技术点的结合:
- 回溯算法(Backtracking) :用于枚举所有分割方案;
- 动态规划预处理(DP Preprocessing) :用于 O(1) 判断任意子串是否为回文。
🧩 动态规划预处理回文表 f[i][j]
我们定义 f[i][j] 表示子串 s[i..j] 是否为回文串。
状态转移方程:
f[i][j] =
true, if i >= j (空串或单字符)
(s[i] == s[j]) && f[i+1][j-1], otherwise
📌 注意遍历顺序:由于
f[i][j]依赖f[i+1][j-1],所以i必须 从后往前 遍历,j从i+1开始往后。
这样预处理后,任意子串是否回文可在 O(1) 时间判断。
🔁 回溯搜索所有分割方案
- 从位置
i = 0开始; - 枚举结束位置
j ∈ [i, n-1]; - 若
f[i][j] == true,说明s[i..j]是回文,加入当前路径; - 递归处理
j+1开始的剩余字符串; - 回溯时弹出刚加入的子串(状态恢复);
- 当
i == n时,说明已处理完整个字符串,将当前路径加入结果集。
✅ 回溯三要素:
- 选择:选
s[i..j]作为下一个回文段;- 约束:只有当
f[i][j]为真才可选;- 目标:
i == n时收集答案。
🧭 解题思路(分步详解)
步骤 1️⃣:预处理所有子串是否为回文
- 创建二维布尔数组
f[n][n],初始化为true(因为i >= j时默认为回文); - 从
i = n-1到0,内层j = i+1到n-1,按 DP 方程填充。
步骤 2️⃣:回溯枚举所有合法分割
-
使用全局变量
ans存储当前路径,ret存储所有结果; -
从
i=0开始 DFS; -
对每个
j,若f[i][j]成立,则:- 将
s.substr(i, j-i+1)加入ans; - 递归
dfs(s, j+1); - 回溯:
ans.pop_back()。
- 将
步骤 3️⃣:返回结果
- 当
i == n,说明完成一次有效分割,将ans拷贝进ret。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n ⋅ 2ⁿ) 最坏情况:所有字符相同(如 "aaaa"),任意切分都合法,共有 2ⁿ⁻¹ 种方案,每种方案需 O(n) 时间构造字符串列表。DP 预处理 O(n²) 可忽略。 |
| 空间复杂度 | O(n²) 主要来自 f 数组(n×n)。回溯栈深 O(n),路径存储 O(n),均低于 O(n²)。 |
| 面试高频点 | ✅ 回溯模板掌握 ✅ DP 预处理优化思想 ✅ 字符串子串提取(substr)性能意识(C++ 中为 O(k),k 为长度) |
💬 面试加分回答:
“为了避免在回溯过程中重复判断回文,我采用动态规划预处理所有子串的回文性,将每次判断从 O(n) 降到 O(1),虽然空间多用了 O(n²),但整体效率显著提升,尤其在字符串较长或回文判断频繁时。”
💻 代码
C++ 实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
private:
vector<vector<int>> f; // f[i][j] 表示 s[i..j] 是否为回文
vector<vector<string>> ret; // 存储所有分割方案
vector<string> ans; // 当前路径
int n; // 字符串长度
public:
void dfs(const string& s, int i) {
if (i == n) { // 已处理完所有字符
ret.push_back(ans); // 将当前方案加入结果
return;
}
for (int j = i; j < n; ++j) {
if (f[i][j]) { // 如果 s[i..j] 是回文
ans.push_back(s.substr(i, j - i + 1)); // 选择该子串
dfs(s, j + 1); // 递归处理剩余部分
ans.pop_back(); // 回溯:撤销选择
}
}
}
vector<vector<string>> partition(string s) {
n = s.size();
f.assign(n, vector<int>(n, true)); // 初始化为 true(i>=j 时成立)
// 动态规划预处理:从下往上填表
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
f[i][j] = (s[i] == s[j]) && f[i + 1][j - 1];
}
}
dfs(s, 0); // 从索引 0 开始回溯
return ret;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
string s1 = "aab";
auto res1 = sol.partition(s1);
// 输出: [["a","a","b"],["aa","b"]]
for (auto& v : res1) {
cout << "[";
for (int i = 0; i < v.size(); ++i) {
cout << """ << v[i] << """;
if (i != v.size() - 1) cout << ",";
}
cout << "]\n";
}
string s2 = "a";
auto res2 = sol.partition(s2);
// 输出: [["a"]]
for (auto& v : res2) {
cout << "[";
for (int i = 0; i < v.size(); ++i) {
cout << """ << v[i] << """;
if (i != v.size() - 1) cout << ",";
}
cout << "]\n";
}
return 0;
}
JavaScript 实现(等效逻辑)
/**
* @param {string} s
* @return {string[][]}
*/
var partition = function(s) {
const n = s.length;
// 预处理 f[i][j]:是否回文
const f = Array.from({ length: n }, () => Array(n).fill(true));
for (let i = n - 1; i >= 0; i--) {
for (let j = i + 1; j < n; j++) {
f[i][j] = (s[i] === s[j]) && f[i + 1][j - 1];
}
}
const ret = [];
const ans = [];
function dfs(i) {
if (i === n) {
ret.push([...ans]); // 深拷贝当前路径
return;
}
for (let j = i; j < n; j++) {
if (f[i][j]) {
ans.push(s.slice(i, j + 1)); // 选择
dfs(j + 1); // 递归
ans.pop(); // 回溯
}
}
}
dfs(0);
return ret;
};
✅ JS 注意点:
- 使用
slice(i, j+1)提取子串(左闭右开);ret.push([...ans])必须深拷贝,否则后续pop会影响已存结果。
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!