热题100 - 131. 分割回文串

76 阅读3分钟

问题描述

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

示例:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

初始思路:回溯法实现

问题分析

面对分割回文串的问题,首先需要解决两个核心问题:

  1. 如何判断子串是回文?
  2. 如何高效生成所有可能的分割方案?

对于第一个问题,常规解法是使用双指针法进行回文判断。第二个问题则需要通过回溯法遍历所有可能的分割方式。

回溯法实现

class Solution {
    public List<List<String>> partition(String s) {
        List<List<String>> res = new ArrayList<>();
        dfs(res, new ArrayList<>(), 0, 1, s);
        return res;
    }

    private void dfs(List<List<String>> res, List<String> holder, 
                    int curr, int step, String s) {
        if (curr + step > s.length()) {
            if (step == 1) { // 成功分割标志
                res.add(new ArrayList<>(holder));
            }
            return;
        }
        String candidate = s.substring(curr, curr + step);
        if (isRe(candidate)) {
            holder.add(candidate);
            dfs(res, holder, curr + step, 1, s); // 处理下一段
            holder.remove(holder.size() - 1);    // 回溯
        }
        dfs(res, holder, curr, step + 1, s);      // 扩展当前子串
    }

    // 双指针判断回文
    private boolean isRe(String s) {
        int left = 0, right = s.length() - 1;
        while (left < right) {
            if (s.charAt(left++) != s.charAt(right--)) {
                return false;
            }
        }
        return true;
    }
}

核心逻辑解析

  1. 步长试探:从长度为1的子串开始尝试分割
  2. 回溯操作:发现有效回文子串后,固定当前分割,继续处理剩余字符串
  3. 分支扩展:若当前分割不成立,则尝试扩展子串长度继续判断

复杂度分析

时间复杂度

  • 回文判断:单次判断时间复杂度为 O(n)
  • 最坏情况:当字符串全为相同字符时,时间复杂度达 O(n² * 2ⁿ)

空间复杂度

  • 递归栈深度为 O(n)
  • 结果存储空间为 O(n * 2ⁿ)

负优化尝试:缓存回文判断

优化思路

通过 HashMap 缓存已判断过的子串,减少重复计算。

实现代码

class Solution {
    // 添加缓存参数
    public List<List<String>> partition(String s) {
        // ...
        Map<String, Boolean> cache = new HashMap<>();
        dfs(..., cache);
        return res;
    }

    private boolean isRe(String s, Map<String, Boolean> cache) {
        if (cache.containsKey(s)) return cache.get(s);
        // 判断逻辑不变,结果存入缓存
    }
}

优化失败原因

  1. 子串数量级为 O(n²),缓存空间占用大
  2. 字符串操作和哈希查询引入额外开销
  3. 实际测试运行时间反而增加

突破性优化:动态规划预处理

优化思路

通过动态规划预处理所有可能的回文子串,将回文判断时间复杂度降至 O(1)。

动态规划实现

class Solution {
    public List<List<String>> partition(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n]; // 回文状态表
        
        // 预处理回文表(从下往上填充)
        for (int i = n-1; i >= 0; i--) {
            for (int j = i; j < n; j++) {
                if (i == j) { // 单字符情况
                    dp[i][j] = true;
                } else if (s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = (j - i == 1) || dp[i+1][j-1];
                }
            }
        }
        
        List<List<String>> res = new ArrayList<>();
        dfs(s, 0, new ArrayList<>(), res, dp);
        return res;
    }

    private void dfs(String s, int start, List<String> path,
                   List<List<String>> res, boolean[][] dp) {
        if (start == s.length()) {
            res.add(new ArrayList<>(path));
            return;
        }
        
        for (int end = start; end < s.length(); end++) {
            if (dp[start][end]) {
                path.add(s.substring(start, end+1));
                dfs(s, end+1, path, res, dp);
                path.remove(path.size()-1);
            }
        }
    }
}

优化亮点

  1. 预处理时间复杂度:O(n²)
  2. 回溯时间复杂度:降至 O(n * 2ⁿ)
  3. 空间换时间:通过 O(n²) 空间换取指数级时间优化

性能对比

方法运行时间(LeetCode测试)时间复杂度
基础回溯9msO(n² * 2ⁿ)
动态规划优化7msO(n² + n*2ⁿ)

总结与思考

  1. 回溯法的适用性:在问题需要遍历所有可能解时,回溯法仍然是直观有效的解决方案
  2. 预处理的价值:动态规划通过空间换时间的策略,显著优化了重复计算问题
  3. 优化陷阱:并非所有缓存策略都能奏效,需要结合具体场景分析
  4. 延伸思考:对于更长的输入字符串,还可以结合记忆化搜索进行进一步优化

学习建议:理解动态规划需要从基础问题入手,推荐从简单的子序列问题开始练习,逐步掌握状态转移的设计技巧。后续可以进一步研究 Manacher 算法等更高效的回文处理算法。