【Leetcode hot 100】131.分割回文串
问题链接
131.分割回文串
问题描述
给你一个字符串 s
,请你将 s
分割成一些 子串,使每个子串都是 回文串 。返回 s
所有可能的分割方案。
示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:
输入:s = “a”
输出:[[“a”]]
提示:
1 <= s.length <= 16
s
仅由小写英文字母组成
问题解答
方法一:基础回溯 + 双指针判断回文
思路解析
- 回溯框架:用
path
存储当前分割出的回文子串,用result
存储所有有效分割方案;start
为当前分割的起始索引(标记已处理完s[0..start-1]
,待处理s[start..n-1]
)。 - 终止条件:当
start == s.length()
时,说明已分割到字符串末尾,将path
的拷贝加入result
(避免引用修改)。 - 遍历与判断:从
start
开始遍历所有可能的结束索引i
,用双指针判断s[start..i]
是否为回文串。若为回文,则将子串加入path
,递归处理start = i+1
,最后回溯(移除path
末尾元素)。
代码实现
import java.util.ArrayList;
import java.util.List;class Solution {// 存储所有有效分割方案private List<List<String>> result = new ArrayList<>();// 存储当前分割的回文子串private List<String> path = new ArrayList<>();public List<List<String>> partition(String s) {// 从起始索引0开始回溯backtrack(s, 0);return result;}/*** 回溯函数* @param s 原始字符串* @param start 当前分割的起始索引*/private void backtrack(String s, int start) {// 终止条件:已分割到字符串末尾if (start == s.length()) {result.add(new ArrayList<>(path)); // 拷贝path,避免后续修改影响结果return;}// 遍历所有可能的结束索引i(从start到末尾)for (int i = start; i < s.length(); i++) {// 判断s[start..i]是否为回文串if (isPalindrome(s, start, i)) {// 若为回文,加入当前路径path.add(s.substring(start, i + 1)); // substring是[左闭右开),需i+1// 递归处理下一段(起始索引更新为i+1)backtrack(s, i + 1);// 回溯:移除最后一个元素,尝试其他分割方式path.remove(path.size() - 1);}}}/*** 双指针判断子串s[left..right]是否为回文串* @param s 原始字符串* @param left 子串左边界* @param right 子串右边界* @return 是回文串返回true,否则false*/private boolean isPalindrome(String s, int left, int right) {while (left < right) {// 左右指针字符不相等,不是回文if (s.charAt(left) != s.charAt(right)) {return false;}left++;right--;}return true;}
}
方法二:回溯 + 动态规划(DP)预处理回文
思路优化点
方法一中,每次判断回文串都用双指针扫描(时间O(n)
),存在重复计算(如s[0..2]
和s[1..1]
会重复判断s[1]
)。
通过DP预处理,提前计算出所有子串s[i..j]
是否为回文串,后续判断可直接查DP数组(时间O(1)
),降低时间复杂度。
具体步骤
- DP数组定义:
dp[i][j]
表示子串s[i..j]
是否为回文串(i <= j
)。 - DP初始化:
- 单个字符(
i == j
):dp[i][i] = true
(必然是回文)。 - 两个字符(
j = i+1
):dp[i][j] = (s.charAt(i) == s.charAt(j))
(字符相等则为回文)。
- 单个字符(
- DP填充:对于长度
>2
的子串(j - i > 1
),需满足两个条件:- 子串首尾字符相等(
s.charAt(i) == s.charAt(j)
)。 - 中间子串
s[i+1..j-1]
是回文(dp[i+1][j-1] = true
)。 - 填充顺序:需从下往上、从左往右遍历
i
(因dp[i][j]
依赖dp[i+1][j-1]
,需先计算i+1
层)。
- 子串首尾字符相等(
- 回溯逻辑:与方法一一致,仅将
isPalindrome
判断替换为dp[start][i]
。
代码实现
import java.util.ArrayList;
import java.util.List;class Solution {private List<List<String>> result = new ArrayList<>();private List<String> path = new ArrayList<>();// DP数组:dp[i][j]表示s[i..j]是否为回文串private boolean[][] dp;public List<List<String>> partition(String s) {int n = s.length();// 初始化DP数组(n x n)dp = new boolean[n][n];// 1. 填充DP数组:单个字符的回文for (int i = 0; i < n; i++) {dp[i][i] = true;}// 2. 填充DP数组:两个字符的回文for (int i = 0; i < n - 1; i++) {dp[i][i + 1] = (s.charAt(i) == s.charAt(i + 1));}// 3. 填充DP数组:长度>2的子串(从下往上、从左往右遍历i)for (int i = n - 3; i >= 0; i--) { // i从倒数第3个字符开始(保证j=i+2存在)for (int j = i + 2; j < n; j++) { // j从i+2开始(长度>=3)dp[i][j] = (s.charAt(i) == s.charAt(j)) && dp[i + 1][j - 1];}}// 开始回溯backtrack(s, 0);return result;}// 回溯函数(与方法一一致,仅回文判断替换为DP数组)private void backtrack(String s, int start) {if (start == s.length()) {result.add(new ArrayList<>(path));return;}for (int i = start; i < s.length(); i++) {// 直接查DP数组判断回文(O(1)时间)if (dp[start][i]) {path.add(s.substring(start, i + 1));backtrack(s, i + 1);path.remove(path.size() - 1);}}}
}
复杂度分析
解法 | 时间复杂度 | 空间复杂度 |
---|---|---|
方法一(基础) | O(n × 2ⁿ) | O(n) |
方法二(DP优化) | O(n² + n × 2ⁿ) | O(n²) |
- 时间:两种解法的核心都是枚举
2ⁿ
种分割方式(每个位置有“分割”或“不分割”两种选择),方法一判断回文需O(n)
,方法二预处理O(n²)
后判断回文O(1)
,整体优化后更高效。 - 空间:方法一的空间来自递归栈(深度
n
)和path
(长度n
);方法二额外增加O(n²)
的DP数组。