当前:LC131 · 分割回文串 · 首次出现于 Day 28 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC131算法模式回溯 · 切割字符串 · path 栈选择树与撤销绳

分割回文串

s="aab",要求把它切成若干段,每一段都必须是回文。期望输出全部合法切法:[["a","a","b"], ["aa","b"]]。

题目是什么

s="aab",要求把它切成若干段,每一段都必须是回文。期望输出全部合法切法:[["a","a","b"], ["aa","b"]]。

解决什么问题

当前路径、可选范围和终止条件分别是什么?

核心结论

切刀模型:维护 start 指针,枚举 end,把候选 s[start:end+1] 试着当作一段;若是回文则 path.push 并递归 end+1,回来后 path.pop 撤销,继续尝试下一种切法。start == n 时收集 path 的拷贝。

01交互算法精讲

先说结论:这道题到底解决什么

怎样从“s="aab",要求把它切成若干段,每一段都必须是回文。期望输出全部合法切法:[["a","a","b"], ["aa","b"]]。”推导出 回溯 · 切割字符串 · path 栈,并证明每次状态变化都不会漏掉答案?

中心结论:切刀模型:维护 start 指针,枚举 end,把候选 s[start:end+1] 试着当作一段;若是回文则 path.push 并递归 end+1,回来后 path.pop 撤销,继续尝试下一种切法。start == n 时收集 path 的拷贝。

读完必须能回答
  1. 1.暴力方案在哪里重复计算,为什么仍然是正确基线?
  2. 2.当前路径、可选范围和终止条件分别是什么?
  3. 3.不变量“path 中保存的每一段子串都已经被验证为回文,向下递归时这条性质继续成立。”为什么能保证算法安全前进?
02交互算法精讲

完整题目与题意拆解

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。

在本站主例中,s="aab",要求把它切成若干段,每一段都必须是回文。期望输出全部合法切法:[["a","a","b"], ["aa","b"]]。

算法最终需要得到或观察:对 s="aab" 收集到 [["a","a","b"], ["aa","b"]] 两种合法切法。

把题目翻译成状态
  • 输入:s="aab",要求把它切成若干段,每一段都必须是回文。期望输出全部合法切法:[["a","a","b"], ["aa","b"]]。
  • 机器需要维护:start(下一刀位置)、end(候选右端)、candidate(s[start:end+1])、isPalindrome、path(已经切下的回文段栈)、ans(结果集)。
  • 最终可观察结果:对 s="aab" 收集到 [["a","a","b"], ["aa","b"]] 两种合法切法。
动画 1 · 题意扫描

先看清算法到底要维护什么

先建立输入、目标、输出和第一批状态,不急着进入模板。

Step 1/20%
题目与输入建立输入、目标与算法心智

尝试每个回文前缀,递归切剩余

正在加载算法场景...
03交互算法精讲

第一层方案:暴力做法

先生成全部结果再过滤会探索大量无效分支;回溯在选择阶段剪枝,并在返回时恢复现场。

暴力方案的价值是确认题意并提供正确性基线。它通常会覆盖所有候选, 但没有保存已经确认的信息,因此同一状态会被重新计算。

动画 2 · 暴力重复

重复工作究竟发生在哪里

把重复读取或重复搜索的区域明确标出,再决定优化必须保存什么。

Step 1/20%
先做对:建立暴力基线枚举所有候选并完整验证

尝试每个回文前缀,递归切剩余

正在加载算法场景...
优化方向:切刀模型:维护 start 指针,枚举 end,把候选 s[start:end+1] 试着当作一段;若是回文则 path.push 并递归 end+1,回来后 path.pop 撤销,继续尝试下一种切法。start == n 时收集 path 的拷贝。
04交互算法精讲

整体地图:先做什么,再做什么

  1. 1建模把输入翻译成“选择树与撤销绳”,明确答案需要观察什么。
  2. 2状态只维护 start(下一刀位置)、end(候选右端)、candidate(s[start:end+1])、isPalindrome、path(已经切下的回文段栈)、ans(结果集)。
  3. 3转移每一步按照 切刀模型:维护 start 指针,枚举 end,把候选 s[start:end+1] 试着当作一段;若是回文则 path.push 并递归 end+1,回来后 path.pop 撤销,继续尝试下一种切法。start == n 时收集 path 的拷贝。
  4. 4收尾读取 对 s="aab" 收集到 [["a","a","b"], ["aa","b"]] 两种合法切法。,并复核边界与复杂度。
05交互算法精讲

选择树与撤销绳:核心概念

路径记录已做选择,候选集合决定下一步,撤销保证兄弟分支互不污染。

这道题不是为了记住一个组件名称,而是为了让状态具有可解释的语义:start(下一刀位置)、end(候选右端)、candidate(s[start:end+1])、isPalindrome、path(已经切下的回文段栈)、ans(结果集)。

核心不变量
  • path 中保存的每一段子串都已经被验证为回文,向下递归时这条性质继续成立。
  • start 指针之前的字符已经被合法切完,未处理的串就是 s[start:]。
  • 每一层递归只枚举从 start 开始的所有回文前缀,保证不会漏切也不会重复切。
  • start == len(s) 时,path 是一种完整的合法分割,把它的拷贝加入 ans。
动画 3 · 核心概念

建立“选择树与撤销绳”心智模型

用主例建立核心状态,先预测下一步,再公开正确分支和理由。

Step 1/30%
题目与输入建立输入、目标与算法心智

尝试每个回文前缀,递归切剩余

正在加载算法场景...
06交互算法精讲

核心机制:状态如何一步步变化

要求输出一个字符串可以被拆成回文串的所有解,DFS 递归求解即可。

切刀模型:维护 start 指针,枚举 end,把候选 s[start:end+1] 试着当作一段;若是回文则 path.push 并递归 end+1,回来后 path.pop 撤销,继续尝试下一种切法。start == n 时收集 path 的拷贝。

执行过程中持续维护:start(下一刀位置)、end(候选右端)、candidate(s[start:end+1])、isPalindrome、path(已经切下的回文段栈)、ans(结果集)。

正确性依赖以下不变量:path 中保存的每一段子串都已经被验证为回文,向下递归时这条性质继续成立。;start 指针之前的字符已经被合法切完,未处理的串就是 s[start:]。;每一层递归只枚举从 start 开始的所有回文前缀,保证不会漏切也不会重复切。;start == len(s) 时,path 是一种完整的合法分割,把它的拷贝加入 ans。

面试时可以压缩为:回溯切割字符串。维护 start 指针表示未处理前缀的起点,枚举 end,若 s[start:end+1] 是回文就把它压入 path 并递归 end+1,回溯后再弹出。start 走到末尾时把 path 拷一份加入结果。回文判断可用区间 DP 预处理降到均摊 O(1)。

落到当前题,执行机制可以压缩为:切刀模型:维护 start 指针,枚举 end,把候选 s[start:end+1] 试着当作一段;若是回文则 path.push 并递归 end+1,回来后 path.pop 撤销,继续尝试下一种切法。start == n 时收集 path 的拷贝。每一次更新都必须保持核心不变量,而不是只让样例碰巧得到正确答案。

动画 4 · 机制构建

一次状态转移为什么成立

集中观察一次选择、计算和状态写回,让变量变化与原因同时出现。

Step 1/50%
切出回文段 'a'path.push(substring)

start=0,end=0

正在加载算法场景...
07交互算法精讲

正确性证明:为什么不会漏答案

初始化

初始化:算法开始时,全部合法候选仍在状态表示范围内;path 中保存的每一段子串都已经被验证为回文,向下递归时这条性质继续成立。

保持

保持:执行“切刀模型:维护 start 指针,枚举 end,把候选 s[start:end+1] 试着当作一段;若是回文则 path.push 并递归 end+1,回来后 path.pop 撤销,继续尝试下一种切法。start == n 时收集 path 的拷贝。”时,只删除已经能证明不可能的候选,并把新信息写回 start(下一刀位置)、end(候选右端)、candidate(s[start:end+1])、isPalindrome、path(已经切下的回文段栈)、ans(结果集)。

终止

终止:没有待处理状态或达到命中条件时,当前可观察结果就是“对 s="aab" 收集到 [["a","a","b"], ["aa","b"]] 两种合法切法。”。

正确性抓手不是“样例跑通”,而是每一帧结束后仍能复述:path 中保存的每一段子串都已经被验证为回文,向下递归时这条性质继续成立。;start 指针之前的字符已经被合法切完,未处理的串就是 s[start:]。;每一层递归只枚举从 start 开始的所有回文前缀,保证不会漏切也不会重复切。;start == len(s) 时,path 是一种完整的合法分割,把它的拷贝加入 ans。
08交互算法精讲

完整执行过程

  1. 1题目与输入s="aab",要求把它切成若干段,每一段都必须是回文。期望输出全部合法切法:[["a","a","b"], ["aa","b"]]。 因为:切刀模型:维护 start 指针,枚举 end,把候选 s[start:end+1] 试着当作一段;若是回文则 path.push 并递归 end+1,回来后 path.pop 撤销,继续尝试下一种切法。start == n 时收集 path 的拷贝。
  2. 2切出回文段 'a'确认前缀回文,递归剩余。 因为:非回文前缀直接剪枝。
  3. 3切出回文段 'a'确认前缀回文,递归剩余。 因为:非回文前缀直接剪枝。
  4. 4切出回文段 'b'确认前缀回文,递归剩余。 因为:非回文前缀直接剪枝。
  5. 5分割方案: a | a | b完整分割。 因为:覆盖全串且无重叠。
  6. 6切出回文段 'b'确认前缀回文,递归剩余。 因为:非回文前缀直接剪枝。
  7. 7分割方案: aa | b完整分割。 因为:覆盖全串且无重叠。
  8. 8收尾与复杂度对 s="aab" 收集到 [["a","a","b"], ["aa","b"]] 两种合法切法。 因为:时间 O(n · 2^n) 含回文判断 O(n),整体 O(n^2 · 2^n);DP 预处理回文后降到 O(n^2 + n · 2^n) · 空间 O(n) 递归栈与 path(DP 预处理回文表则需 O(n^2))。切刀模型:维护 start 指针,枚举 end,把候选 s[start:end+1] 试着当作一段;若是回文则 path.push 并递归 end+1,回来后 path.pop 撤销,继续尝试下一种切法。start == n 时收集 path 的拷贝。
动画 5 · 完整执行

从输入完整走到可观察结果

从主例第一帧运行到答案,时间轴始终显示当前状态、因果解释和下一步。

Step 1/80%
题目与输入建立输入、目标与算法心智

尝试每个回文前缀,递归切剩余

正在加载算法场景...
09交互算法精讲

把动画和 Go 代码逐行对应

代码窗口不会按容易漂移的固定数字行号硬绑动画,而是根据当前语义阶段, 在完整 Go 实现中定位选择、计算、写回或返回分支。当前变量与高亮行一起变化。

动画 6 · 代码映射

让每个动作都落到 Go 分支

重新执行关键分支,只显示当前代码附近窗口,并说明这行为什么在此刻运行。

Step 1/60%
切分起点 startbacktrack(start)

尝试每个回文前缀,递归切剩余

正在加载算法场景...
10交互算法精讲

完整 Go 提交代码与最小测试

完整 Go 解法
// 解法一
func partition(s string) [][]string {
	if s == "" {
		return [][]string{}
	}
	res, pal := [][]string{}, []string{}
	findPalindrome(s, 0, "", true, pal, &res)
	return res
}

func findPalindrome(str string, index int, s string, isPal bool, pal []string, res *[][]string) {
	if index == len(str) {
		if isPal {
			tmp := make([]string, len(pal))
			copy(tmp, pal)
			*res = append(*res, tmp)
		}
		return
	}
	if index == 0 {
		s = string(str[index])
		pal = append(pal, s)
		findPalindrome(str, index+1, s, isPal && isPalindrome(s), pal, res)
	} else {
		temp := pal[len(pal)-1]
		s = pal[len(pal)-1] + string(str[index])
		pal[len(pal)-1] = s
		findPalindrome(str, index+1, s, isPalindrome(s), pal, res)
		pal[len(pal)-1] = temp
		if isPalindrome(temp) {
			pal = append(pal, string(str[index]))
			findPalindrome(str, index+1, temp, isPal && isPalindrome(temp), pal, res)
			pal = pal[:len(pal)-1]

		}
	}
	return
}

func isPalindrome(s string) bool {
	slen := len(s)
	for i, j := 0, slen-1; i < j; i, j = i+1, j-1 {
		if s[i] != s[j] {
			return false
		}
	}
	return true
}

// 解法二
func partition131_1(s string) [][]string {
	result := [][]string{}
	size := len(s)
	if size == 0 {
		return result
	}
	current := make([]string, 0, size)
	dfs(s, 0, current, &result)
	return result
}

func dfs(s string, idx int, cur []string, result *[][]string) {
	start, end := idx, len(s)
	if start == end {
		temp := make([]string, len(cur))
		copy(temp, cur)
		*result = append(*result, temp)
		return
	}
	for i := start; i < end; i++ {
		if isPal(s, start, i) {
			dfs(s, i+1, append(cur, s[start:i+1]), result)
		}
	}
}

func isPal(str string, s, e int) bool {
	for s < e {
		if str[s] != str[e] {
			return false
		}
		s++
		e--
	}
	return true
}
最小测试集合
func main() {
    // 1. 主例
    //    输入:mode="palindrome-partition", text="aab"
    //    期望:对 s="aab" 收集到 [["a","a","b"], ["aa","b"]] 两种合法切法。
    //
    // 2. 失败 / 未命中
    //    检查:复杂度误写成 O(n):合法分割数本身可能是指数级,结果数量决定下界。
    //
    // 3. 边界
    //    空输入、单元素、最小合法规模,以及答案恰好落在边界的情况。
    //
    // 4. 迁移
    //    LC132 分割回文串 II(同切割模型 + DP 替换回溯);LC93 复原 IP 地址(切刀 + 段长 ≤ 3 + 段值 ≤ 255 的合法性剪枝);LC139 单词拆分(切刀 + 字典判定 + 记忆化)
}

Go 参考实现基于 halfrost/LeetCode-Go 的 MIT 许可代码整理,并按本站教学结构补充解释与动画映射。

11交互算法精讲

正确性与复杂度

时间复杂度 O(n · 2^n) 含回文判断 O(n),整体 O(n^2 · 2^n);DP 预处理回文后降到 O(n^2 + n · 2^n)

执行过程中只保留仍可能影响答案的状态。切刀模型:维护 start 指针,枚举 end,把候选 s[start:end+1] 试着当作一段;若是回文则 path.push 并递归 end+1,回来后 path.pop 撤销,继续尝试下一种切法。start == n 时收集 path 的拷贝。

空间复杂度 O(n) 递归栈与 path(DP 预处理回文表则需 O(n^2))

额外状态主要用于维护:start(下一刀位置)、end(候选右端)、candidate(s[start:end+1])、isPalindrome、path(已经切下的回文段栈)、ans(结果集)。

终局不变量
  • path 中保存的每一段子串都已经被验证为回文,向下递归时这条性质继续成立。
  • start 指针之前的字符已经被合法切完,未处理的串就是 s[start:]。
  • 每一层递归只枚举从 start 开始的所有回文前缀,保证不会漏切也不会重复切。
  • start == len(s) 时,path 是一种完整的合法分割,把它的拷贝加入 ans。
12交互算法精讲

最容易写错的地方

错误 1

复杂度误写成 O(n):合法分割数本身可能是指数级,结果数量决定下界。

错误 2

递归回来忘记 path.pop,导致 path 污染兄弟分支,结果集出现非法或重复方案。

错误 3

把 path 直接 append 进 ans,而不是 append(path 的拷贝),结果集随递归一起被清空。

错误 4

每次都重复 O(n) 判断回文,未用 dp[i][j] = s[i]==s[j] && dp[i+1][j-1] 预处理。

边界复查

必须额外检查空输入、单元素、未命中或不可达情况,以及恰好落在边界的输入。

13交互算法精讲

最后复盘:带走逻辑链

  1. 1题意s="aab",要求把它切成若干段,每一段都必须是回文。期望输出全部合法切法:[["a","a","b"], ["aa","b"]]。
  2. 2重复先生成全部结果再过滤会探索大量无效分支;回溯在选择阶段剪枝,并在返回时恢复现场。
  3. 3优化切刀模型:维护 start 指针,枚举 end,把候选 s[start:end+1] 试着当作一段;若是回文则 path.push 并递归 end+1,回来后 path.pop 撤销,继续尝试下一种切法。start == n 时收集 path 的拷贝。
  4. 4证明path 中保存的每一段子串都已经被验证为回文,向下递归时这条性质继续成立。;start 指针之前的字符已经被合法切完,未处理的串就是 s[start:]。;每一层递归只枚举从 start 开始的所有回文前缀,保证不会漏切也不会重复切。;start == len(s) 时,path 是一种完整的合法分割,把它的拷贝加入 ans。
  5. 5复杂度时间 O(n · 2^n) 含回文判断 O(n),整体 O(n^2 · 2^n);DP 预处理回文后降到 O(n^2 + n · 2^n),空间 O(n) 递归栈与 path(DP 预处理回文表则需 O(n^2))
面试表达:回溯切割字符串。维护 start 指针表示未处理前缀的起点,枚举 end,若 s[start:end+1] 是回文就把它压入 path 并递归 end+1,回溯后再弹出。start 走到末尾时把 path 拷一份加入结果。回文判断可用区间 DP 预处理降到均摊 O(1)。
迁移练习
  • LC132 分割回文串 II(同切割模型 + DP 替换回溯)
  • LC93 复原 IP 地址(切刀 + 段长 ≤ 3 + 段值 ≤ 255 的合法性剪枝)
  • LC139 单词拆分(切刀 + 字典判定 + 记忆化)