题目描述

✅ 131. 分割回文串

image-20260928215040650

题意分析

把字符串 s 从头到尾切成若干非空的连续子串,要求每一段从左到右和从右到左读都相同,并返回所有满足条件的切法。每个字符必须恰好属于一段,不能跳过、重复使用或改变顺序。

这是枚举全部方案,不能在找到一种切法后停止,也不是求最少切割次数。长度为 $n$ 的字符串有 $n-1$ 个可切割间隙,最坏情况下每个间隙都能独立选择切或不切。题目给出 $n\le 16$,可以用回溯逐段枚举,并预先记录哪些区间是回文。

解法:回溯切分 + 回文预处理

核心思路

[!blue]

在下一段从 start 开始时,依次尝试所有结束位置 end,只把回文区间加入当前路径。不同前缀切法可能反复查询同一个区间是否回文,因此先用动态规划建立 pal[i][j],表示闭区间 s[i..j] 是否为回文,将搜索中的每次判断变为 $O(1)$。

一个区间是回文,当且仅当两端字符相同,且去掉两端后的内部区间也是回文。长度为 $1$ 或 $2$ 时没有需要继续检查的内部区间,所以转移为:

\[pal[i][j]=(s[i]=s[j])\land\bigl(j-i<2\ \lor\ pal[i+1][j-1]\bigr)\]

按左端点 i 从大到小计算,依赖的第 i+1 行就已经就绪。代码中的短路或会先处理长度为 $1$、$2$ 的情况,不会访问不存在的内部区间。

回溯状态 dfs(start) 表示:path 中每一段都是回文,它们恰好拼成前缀 s[0..start),start 是第一个尚未分配的字符。选择 s[start..end] 后,下一段只能从 end+1 开始,这样既不重叠也不遗漏。递归返回后移除刚加入的段,恢复到选择前的状态,再尝试其他末尾。

当 start == n 时,路径已经完整覆盖字符串,复制它加入答案。任意合法切法都有唯一的一组分段末尾,回溯会逐一枚举到这些选择;反过来,只有回文段能进入路径,且只有覆盖完整字符串后才会记录。因此得到的恰好是所有合法切法,不会重复。

解题步骤

  1. 建立 pal[n][n],按 i 从大到小、j 从 i 到 n - 1 的顺序预处理所有回文区间。
  2. 用空路径开始 dfs(0)。若 start == n,复制当前路径到答案并返回,不能继续选择下一段。
  3. 枚举 end 从 start 到 n - 1,用 pal[start][end] 判断候选段;不是回文就尝试下一个末尾。
  4. 对回文区间执行“加入路径 → 递归 end + 1 → 移除当前段”,直到所有末尾都尝试完。

每段至少包含一个字符,所以递归中的 start 严格增加,最多深入 $n$ 层。单个字符一定是回文,因此沿着逐字符切分的分支始终能得到合法方案;某个较短区间不是回文,也不代表延长后的区间不是回文,不能提前结束当前循环。

代码实现

class Solution {
    public List<List<String>> partition(String s) {
        int n = s.length();
        boolean[][] pal = new boolean[n][n];

        // 左端倒序,当前回文判断依赖的内部区间已经计算。
        for (int i = n - 1; i >= 0; i--) {
            for (int j = i; j < n; j++) {
                pal[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 2 || pal[i + 1][j - 1]);
            }
        }

        List<List<String>> res = new ArrayList<>();

        dfs(0, s, pal, new ArrayList<>(), res);

        return res;
    }

    private void dfs(
            int start, String s, boolean[][] pal, List<String> path, List<List<String>> res) {
        if (start == s.length()) {
            // 记录完整切分的快照,后续回溯不能改写已收集路径。
            res.add(new ArrayList<>(path));

            return;
        }

        for (int end = start; end < s.length(); end++) {
            if (!pal[start][end]) {
                continue;
            }

            path.add(s.substring(start, end + 1));
            dfs(end + 1, s, pal, path, res);
            // 撤销当前段,再尝试同一起点的其他结束位置。
            path.remove(path.size() - 1);
        }
    }
}
func partition(s string) [][]string {
    n := len(s)
    pal := make([][]bool, n)
    for i := range pal {
        pal[i] = make([]bool, n)
    }
    // 左端倒序,当前回文判断依赖的内部区间已经计算。
    for i := n - 1; i >= 0; i-- {
        for j := i; j < n; j++ {
            pal[i][j] = s[i] == s[j] && (j-i < 2 || pal[i+1][j-1])
        }
    }

    res := make([][]string, 0)
    path := make([]string, 0)
    var dfs func(int)
    dfs = func(start int) {
        if start == n {
            // 记录完整切分的快照,后续回溯不能改写已收集路径。
            cur := append([]string(nil), path...)
            res = append(res, cur)
            return
        }
        for end := start; end < n; end++ {
            if !pal[start][end] {
                continue
            }
            path = append(path, s[start:end+1])
            dfs(end + 1)
            // 撤销当前段,再尝试同一起点的其他结束位置。
            path = path[:len(path)-1]
        }
    }
    dfs(0)
    return res
}

复杂度分析

  • 时间复杂度:$O(n^2 + n \cdot 2^n)$。回文表共有 $O(n^2)$ 个区间;当所有字符相同时,$n-1$ 个间隙的任意切法都合法,共有 $2^{n-1}$ 个答案,每份答案包含至多 $n$ 段。搜索、构造子串和复制路径的总开销由 $O(n\cdot 2^n)$ 限定。
  • 空间复杂度:不计答案为 $O(n^2)$。回文表占 $O(n^2)$,递归栈和当前路径占 $O(n)$;全部答案本身最坏占 $O(n \cdot 2^n)$,这是返回所有方案所需的空间。

关键点总结

[!green]

  • 回文表回答“这一段能否选”,回溯回答“选完这一段后,还能怎样完整切分”。
  • start 左侧已经处理完毕,下一段固定从 start 开始,只需枚举结束位置。
  • 路径在分支之间复用,递归返回时撤销选择,保存答案时复制列表,两步分别保证分支与结果互不干扰。

易错点总结

[!yellow]

  • 直接把可变的 path 放入答案会被后续回溯修改,必须复制。
  • 递归起点应为 end + 1,否则同一字符被重复处理甚至无限递归。
  • 选择后忘记撤销,会让兄弟分支共享错误路径。
  • substring(start, end + 1) 的右边界是开区间;非回文时应 continue,不能终止整层搜索。

相似题目

题目 难度 关联与区别
132. 分割回文串 II 困难 回文区间判定相同,原题求最少切割次数,本题输出所有合法划分。
647. 回文子串 中等 同样预处理或枚举回文子串,本题把这些区间作为划分搜索中的可选边。
5. 最长回文子串 中等 用区间或中心扩展刻画回文结构;本题预处理回文后枚举切分方案,该题寻找最长连续回文。
516. 最长回文子序列 中等 用区间或中心扩展刻画回文结构;本题预处理回文后枚举切分方案,该题允许跳过字符求最长回文子序列。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/56752970
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!