给你一个字符串 s,请你将 **s **分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例 1:
输入: s = "aab"
输出: [["a","a","b"],["aa","b"]]
示例 2:
输入: s = "a"
输出: [["a"]]
提示:
1 <= s.length <= 16s仅由小写英文字母组成
1. 生活案例:切“对称”的甘蔗
想象你有一根很长的甘蔗(字符串),上面刻着一些字母。老板要求你把它切成几小段。
-
规则:每一段必须是对称的(比如
aba、cc、a)。 -
过程:
- 你先试着从前面切下一小块。
- 如果这一块是对称的(回文),你就把这一块放进你的篮子里(
path),然后对剩下的甘蔗重复这个过程。 - 如果剩下的甘蔗也能切成全是回文的段,你就成功找到了一种方案。
- 关键点(回溯) :如果你切完发现后面没法切出回文了,你会把刚才放进篮子里的那一块拿出来,试着切一个更长一点的块试试。
2. 代码实现与详细注释
这是你图片中的代码,我为你添加了回溯逻辑的详细中文注释:
JavaScript
/**
* @param {string} s
* @return {string[][]}
*/
var partition = function(s) {
let res = []; // 【总仓库】:存放所有成功的分割方案
let path = []; // 【当前篮子】:存放当前正在尝试的分割组合
// 辅助函数:判断是不是回文(对称)
// 就像检查甘蔗段左右两边字母是否对应
function isPalindrome(str, left, right) {
while (left < right) {
if (str[left] !== str[right]) {
return false; // 不对称
}
left++;
right--;
}
return true; // 是回文
}
// 核心函数:回溯(递归)
function backtrack(start) {
// 【终止条件】:如果切到了甘蔗的最末尾,说明这是一种成功的方案
if (start === s.length) {
res.push([...path]); // 把篮子里的组合复制一份存入仓库
return;
}
// 从当前位置 start 开始,尝试往后切每一个可能的长度 i
for (let i = start; i < s.length; i++) {
// 如果从 start 到 i 这一段是回文
if (isPalindrome(s, start, i)) {
// 1. 【做选择】:切下这一段,放进篮子
path.push(s.substring(start, i + 1));
// 2. 【递归】:去切剩下的那部分甘蔗
backtrack(i + 1);
// 3. 【撤销选择】:也就是回溯。
// 把最后切的那块拿出来,让循环继续,尝试切一个更长的块。
path.pop();
}
}
}
backtrack(0); // 从位置 0 开始切
return res;
};
3. 核心原理解析
为什么需要 path.pop()?
这就是回溯的灵魂。
假设字符串是 "aab":
- 第一步切下第一个
"a",放入篮子,path = ["a"]。 - 递归去处理后面的
"ab"。 - 发现后面可以切出
"a"和"b"。得到一种方案["a", "a", "b"]。 - 关键时刻:为了寻找其他方案(比如开头直接切
"aa"),你必须把篮子里最后的那个"b"拿出来,再把"a"拿出来。这就是pop,它让程序能够回到之前的状态,去尝试i增加后的新可能。
递归树的视角
这个过程可以看作一棵树:
- 每一层:决定当前这一刀切在哪里。
- 分叉:如果切下的部分是回文,就分出一个支流往下走。
- 尽头:如果切不动了且甘蔗用完了,记录结果;如果切下的不是回文,这个分叉就死掉。
复杂度分析
- 时间复杂度:最坏情况下是 ,因为每个位置都可以选切或不切,且每次都要判断回文。
- 空间复杂度:,主要是递归栈的深度和
path篮子的长度。