1486 words
7 minutes
Leetcode Hot 100 分割回文串

Hot 100 又来一道标准回溯题,名字听着像文艺片,做起来像切西瓜:

把字符串切成若干段,并且每一段都得是回文串。

这题不难,但很适合拿来练“回溯到底在搜什么”。 你要搜的不是某个最优值,而是:

  • 当前这一段从哪里开始
  • 这一刀切在哪里结束
  • 切出来的这一段是不是回文

只要这三件事理清楚,代码就不会写成一锅回文粥。

题目链接#

LeetCode 131. 分割回文串

题目描述#

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

分割回文串题目截图

解题思路:回溯枚举每一段的结束位置#

这题最自然的想法就是:

  • 当前从位置 start 开始切
  • 枚举这一段可以在哪个位置 end 结束
  • 如果 s[start:end+1] 是回文串,就把它加入路径
  • 然后递归处理后面的部分

start == len(s) 的时候,说明整个字符串已经被切完,当前路径就是一种合法答案。

一句话总结它的灵魂:

从左到右切,每次只要当前这段是回文,就递归去切后面。

为什么这是回溯#

因为每次做选择时,你都在决定:

  • 当前这一段到底切多长

比如字符串 aab

从下标 0 开始时,可以尝试:

  • a
  • aa
  • aab

其中:

  • a 是回文,可以继续
  • aa 也是回文,也可以继续
  • aab 不是回文,直接跳过

于是搜索树就长这样:

start=0
├── "a"
│ └── start=1
│ ├── "a"
│ │ └── start=2
│ │ └── "b" → ["a", "a", "b"]
│ └── "ab" ×
└── "aa"
└── start=2
└── "b" → ["aa", "b"]

所以最后答案就是:

[["a", "a", "b"], ["aa", "b"]]

代码实现#

class Solution(object):
def partition(self, s):
"""
:type s: str
:rtype: List[List[str]]
"""
n = len(s)
ans = []
path = []
def is_pal(sub):
return sub == sub[::-1]
def dfs(start):
if start == n:
ans.append(path[:])
return
for end in range(start, n):
if is_pal(s[start:end + 1]):
path.append(s[start:end + 1])
dfs(end + 1)
path.pop()
dfs(0)
return ans

代码拆解#

1. dfs(start) 表示什么#

这里的 start 表示:

当前这一段从哪个位置开始切。

比如 start = 2,就意味着前面的部分已经处理完了,现在只需要考虑 s[2:] 怎么切。

2. 为什么 start == n 就可以加入答案#

if start == n:
ans.append(path[:])
return

start 走到字符串末尾时,说明整个字符串已经被完整分割,且前面加入 path 的每一段都经过了回文校验。

所以当前路径就是一种合法方案。

3. 为什么要枚举 end#

for end in range(start, n):

因为当前这一段可能是:

  • 一个字符
  • 两个字符
  • 三个字符
  • ……
  • 一直到字符串末尾

也就是说,我们在尝试“这一刀到底切到哪里”。

4. 只有是回文才继续递归#

if is_pal(s[start:end + 1]):
path.append(s[start:end + 1])
dfs(end + 1)
path.pop()

这是整题的门槛。

如果当前这一段不是回文,那这条路就没必要继续走了,直接剪掉。

如果是回文:

  1. 加入当前路径
  2. 递归处理下一段
  3. 回溯撤销选择

标准回溯三连,稳得像老油条。

你的“切 / 不切”思路也能做#

有些人会写成:

  • 遍历到位置 i
  • 选择“在这里切一刀”
  • 或者“先不切,继续往后延长当前段”

这也没问题,本质上还是在枚举切割方案。

不过在表达上,通常“枚举当前段结束位置”会更清晰,因为:

  • 状态更直接
  • 边界更好理解
  • 代码结构也更标准

所以写题解时,更推荐上面那版。

复杂度分析#

时间复杂度#

这题需要枚举所有可能的分割方案,而一个字符串的切分方式本身就是指数级的。

因此时间复杂度通常可以写为:

O(n * 2^n)

其中额外的 n 来自回文判断和路径构造。

空间复杂度#

递归栈深度最多为 n,路径长度最多也为 n,所以空间复杂度为:

O(n)

如果把结果数组也算上,那最终空间还要加上答案本身的总规模。

可以怎么优化#

上面代码里,每次都用:

sub == sub[::-1]

来判断回文,写起来很舒服,但会重复计算。

如果想进一步优化,可以先用 dp[i][j] 预处理:

  • dp[i][j] = True 表示 s[i:j+1] 是回文串

这样回溯时就能 O(1) 判断当前子串是否为回文。

不过这题在面试或刷题场景里,先把基础回溯写对更重要,别还没切瓜,先把菜板升级成 CNC 机床了。

易错点总结#

1. 终止条件写错#

有些人会把终止条件写成“枚举到最后一个字符”。

其实更准确的是:

当起点 start 走到字符串末尾时,说明整个分割完成。

2. 回文判断通过后忘记回溯#

path.append(...)
dfs(...)
path.pop()

这一进一出必须成对出现,不然路径会串味,前面切的瓜会混进后面的果盘。

3. 空字符串返回值写错#

如果你特地处理空串,也应该返回列表类型:

return []

不要返回 False,题目没让你演真假美猴王。

小结#

这题的核心非常纯粹:

回溯枚举每一段的结束位置,只要当前子串是回文,就继续切后面的部分。

所以整题就是两步:

  1. 枚举这一段切到哪里
  2. 判断这一段是不是回文

只要当前段合法,就递归; 走到字符串末尾,就收集答案。

这就是 131 题的全部秘密。

别看题目叫“分割回文串”,本质上就是一句话:

刀法要稳,切的每一块都得左右对称。 🦐

Leetcode Hot 100 分割回文串
https://ssonnyboy.github.io/yoroziya/posts/leetcode-hot-100-palindrome-partitioning/
Author
biabuluo
Published at
2026-04-10
License
CC BY-NC-SA 4.0