力扣题目-分割回文串

78 阅读6分钟

一、题目描述

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

  示例 1:

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

示例 2:

输入: s = "a"
输出: [["a"]]

 

提示:

  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成

二、解答

题目分析

  1. 问题描述

    • 给定一个字符串 s,需要将其分割成若干子串,且每个子串都要是回文串(即正读和反读都一样的字符串)。
    • 最终要返回所有可能的符合条件的分割方案。每个分割方案表示为一个字符串数组,数组中的每个元素是一个回文子串。
  2. 输入输出要求

    • 输入是一个字符串 s,其长度范围是 1 <= s.length <= 16,并且 s 仅由小写英文字母组成。
    • 输出是一个二维字符串数组,其中每一个子数组表示一种可能的分割方案,子数组中的元素是回文子串。
  3. 问题难点

    • 如何确定所有可能的分割方式,因为分割点的选择有多种可能性。
    • 对于每一种分割方式,需要判断分割出的子串是否为回文串。
    • 要收集所有符合条件的分割方案,不能遗漏任何一种可能的情况。

示例分析

  1. 示例 1

    • 输入 s = "aab"

    • 对于字符串 "aab",存在两种合法的分割方式:

      • 第一种分割方式是将其分割成 ["a","a","b"],其中 "a" 是回文串,"a" 是回文串,"b" 也是回文串,满足每个子串都是回文串的要求。
      • 第二种分割方式是将其分割成 ["aa","b"]"aa" 是回文串,"b" 是回文串,也满足条件。
    • 所以输出 [["a","a","b"],["aa","b"]]

  2. 示例 2

    • 输入 s = "a"
    • 对于字符串 "a",只有一种分割方式,即将其看作一个整体 "a",而 "a" 本身就是一个回文串。
    • 所以输出 [["a"]]

代码

class Solution {
    boolean[][] f;
    List<List<String>> ret = new ArrayList<List<String>>();
    List<String> ans = new ArrayList<String>();
    int n;

    public List<List<String>> partition(String s) {
        n = s.length();
        f = new boolean[n][n];
        for (int i = 0; i < n; ++i) {
            Arrays.fill(f[i], true);
        }

        for (int i = n - 1; i >= 0; --i) {
            for (int j = i + 1; j < n; ++j) {
                f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
            }
        }

        dfs(s, 0);
        return ret;
    }

    public void dfs(String s, int i) {
        if (i == n) {
            ret.add(new ArrayList<String>(ans));
            return;
        }
        for (int j = i; j < n; ++j) {
            if (f[i][j]) {
                ans.add(s.substring(i, j + 1));
                dfs(s, j + 1);
                ans.remove(ans.size() - 1);
            }
        }
    }
}


代码分析


class Solution {
    // f 是一个二维布尔数组,用于记录字符串 s 中从索引 i 到索引 j 的子串是否为回文串
    boolean[][] f;
    // ret 用于存储所有可能的分割方案,每个方案是一个字符串列表
    List<List<String>> ret = new ArrayList<List<String>>();
    // ans 用于临时存储当前正在构建的分割方案
    List<String> ans = new ArrayList<String>();
    // n 表示字符串 s 的长度
    int n;

    public List<List<String>> partition(String s) {
        // 获取字符串 s 的长度
        n = s.length();
        // 初始化二维布尔数组 f,用于记录子串是否为回文串
        f = new boolean[n][n];
        // 先将 f 数组的所有元素初始化为 true,即默认所有子串都是回文串
        for (int i = 0; i < n; ++i) {
            Arrays.fill(f[i], true);
        }

        // 动态规划计算所有子串是否为回文串
        // 从后往前遍历,这样可以利用之前计算的结果
        for (int i = n - 1; i >= 0; --i) {
            // j 从 i+1 开始,因为当 i == j 时,单个字符一定是回文串,已经初始化为 true
            for (int j = i + 1; j < n; ++j) {
                // 判断从 i 到 j 的子串是否为回文串
                // 条件是当前字符 s.charAt(i) 等于 s.charAt(j),并且去掉首尾字符后的子串 f[i + 1][j - 1] 也是回文串
                f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
            }
        }

        // 从索引 0 开始进行深度优先搜索
        dfs(s, 0);
        // 返回所有可能的分割方案
        return ret;
    }

    public void dfs(String s, int i) {
        // 当 i 等于 n 时,说明已经遍历完整个字符串,找到了一种有效的分割方案
        if (i == n) {
            // 将当前的分割方案 ans 复制一份添加到 ret 中
            ret.add(new ArrayList<String>(ans));
            return;
        }
        // 从索引 i 开始,尝试所有可能的分割点 j
        for (int j = i; j < n; ++j) {
            // 如果从 i 到 j 的子串是回文串
            if (f[i][j]) {
                // 将该回文子串添加到当前的分割方案 ans 中
                ans.add(s.substring(i, j + 1));
                // 递归调用 dfs 函数,从 j + 1 开始继续分割
                dfs(s, j + 1);
                // 回溯操作,移除最后添加的子串,尝试其他可能的分割方案
                ans.remove(ans.size() - 1);
            }
        }
    }
}

类与成员变量

class Solution {
    boolean[][] f;
    List<List<String>> ret = new ArrayList<List<String>>();
    List<String> ans = new ArrayList<String>();
    int n;
}
  • f:这是一个二维布尔数组,f[i][j] 用于标记字符串从索引 i 到索引 j 的子串是否为回文串。
  • ret:这是一个存储最终结果的二维列表,每个子列表代表一种可能的分割方案,其中每个元素是一个回文子串。
  • ans:这是一个临时列表,用于在深度优先搜索过程中构建当前的分割方案。
  • n:表示输入字符串的长度。

partition 方法

public List<List<String>> partition(String s) {
    n = s.length();
    f = new boolean[n][n];
    for (int i = 0; i < n; ++i) {
        Arrays.fill(f[i], true);
    }

    for (int i = n - 1; i >= 0; --i) {
        for (int j = i + 1; j < n; ++j) {
            f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
        }
    }

    dfs(s, 0);
    return ret;
}
  1. 初始化

    • 首先获取输入字符串 s 的长度 n
    • 接着创建并初始化二维布尔数组 f,将所有元素初始化为 true,这意味着默认所有子串都是回文串。
  2. 动态规划计算回文子串

    • 采用两层嵌套循环来填充 f 数组。外层循环从字符串末尾开始向前遍历,内层循环从当前外层循环索引的下一个位置开始往后遍历。
    • f[i][j] 的值由两个条件决定:当前位置 i 和 j 的字符是否相等,以及去掉首尾字符后的子串 f[i + 1][j - 1] 是否为回文串。
  3. 深度优先搜索

    • 调用 dfs 方法,从字符串的起始位置(索引为 0)开始进行深度优先搜索。
    • 最后返回存储所有分割方案的 ret 列表。

dfs 方法

public void dfs(String s, int i) {
    if (i == n) {
        ret.add(new ArrayList<String>(ans));
        return;
    }
    for (int j = i; j < n; ++j) {
        if (f[i][j]) {
            ans.add(s.substring(i, j + 1));
            dfs(s, j + 1);
            ans.remove(ans.size() - 1);
        }
    }
}
  1. 递归终止条件

    • 当 i 等于 n 时,表明已经遍历完整个字符串,此时将当前的分割方案 ans 复制一份添加到 ret 列表中,并终止当前递归调用。
  2. 尝试所有可能的分割点

    • 使用 for 循环从当前索引 i 开始,尝试所有可能的分割点 j
    • 若 f[i][j] 为 true,说明从索引 i 到 j 的子串是回文串,将该子串添加到 ans 列表中。
    • 递归调用 dfs 方法,从 j + 1 开始继续分割字符串。
    • 进行回溯操作,移除 ans 列表中最后添加的子串,以便尝试其他可能的分割方案。

代码总结

代码使用了动态规划和深度优先搜索(DFS)的方法来解决将字符串分割成回文子串的问题。首先通过动态规划预处理出所有子串是否为回文串,然后使用 DFS 递归地尝试所有可能的分割方案,并将符合条件的方案添加到结果列表中。