0、参考资料
一、题目描述
给定一个字符串 s ,请将 s 分割成一些子串,使每个子串都是 回文串 ,返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
输入:s = “google”
输出:[[“g”,“o”,“o”,“g”,“l”,“e”],[“g”,“oo”,“g”,“l”,“e”],[“goog”,“l”,“e”]]
二、代码思路
这题第一眼看,发现很好处理。
枚举出所有子字符串是回文串然后组合即可。
枚举出所有子字符串确实很简单,但是组合你如何处理呢?传统的暴力很难解决的,只能从算法和数据结构上找比较好的解法。
所以大体思路也跟上面差不多,但是细节有很大的出入。
首先,我们需要前置知识如何找出所有的回文子串,这里我们可以使用动态规划的想法,使用一个dp[][]二维数组表,来存储相应的结果。详细思路,看我的上一篇文章。
其次,我们有了标记回文子串的二维数组表,我们如何才能枚举出所有回文子串的组合呢?并且这些组合最终能形成一个完整的字符串。也就是将这个字符串拆成一些回文子串的组合形式。 这里我们就用到了回溯算法:
这里的回溯算法思路是这样的:假如我们有一个字符串“ggag”我们每次都要选一个回文子串,然后基于这个回文子串的选择,再从剩下的串中选择另一个回文子串。直到选完为止。然后退出这一层,并回溯到上一层,观察我选择这个子串之后还有另一种选择的可能吗?其实有点不好描述,结合代码和图应该会很好理解。
因为每一层都是从当前pos出发,让i一直走,这样就能知道从pos开始的所有子串中的所有回文串;同理,从第二个出发再找。如此回溯。
三、代码题解
class Solution {
private List<String> path = new ArrayList();
private List<List<String>> res = new ArrayList();
private String s;
private int n;
private int dp[][];
public String[][] partition(String s) {
//暴力解法:
//选出所有子字符串,然后逐个判断。
//回溯法(可以动态地选出所有子串) + 动态规划法(可以很好判断某一个字符串是不是回文串,并且将结果预处理到表格中)
this.s = s;
this.n = s.length();
this.dp = new int[s.length()][s.length()];
//DP预处理
dp(s);
//回溯枚举出所有可能的子串,注意!我这里描述的不准确:这里的回溯不是简单的回溯,和我们之前做的回溯有本质上区别。
//我们之前的回溯无论是多叉还是二叉都是一个个元素来选择的,但是这里的回溯不是选择单个元素形成一个分叉,而是选择一串子串作为一个分叉,我画个图请看。
dfs(0);
String[][] ans = new String[res.size()][];
for (int i = 0; i < res.size(); i++) {
ans[i] = res.get(i).toArray(new String[res.get(i).size()]);
}
return ans;
}
//DP算法
public void dp(String str) {
for (int i = 0; i < s.length(); i++) {
for (int j = 0; j <= i; j++) {
if ((s.charAt(i) == s.charAt(j)) && (((i - j) < 2) || dp[j + 1][i - 1] == 1)) {
dp[j][i] = 1;
}
}
}
//System.out.println(count);
}
//回溯算法
public void dfs(int cur) {
if (cur == n) {
res.add(new ArrayList(path));
return;
}
for (int i = cur; i < n; i++) {
if (dp[cur][i] == 1) {
path.add(s.substring(cur, i + 1));
//注意,这里的回溯是选择回文子串,而并非是选择单个字符。
//所以,这里并不能是 cur + 1,而应该是i + 1.
//原因就是:cur - i 已经成为一个子串了,那么证明该字串被选。
//那么我们就应该从i + 1,开始再去选择下一个子串。
//这个思想很重要。
dfs(i + 1);
path.remove(path.size() - 1);
}
}
}
}