题目想要实现的效果,很多人拿到这道题,根本不知道示例表达的是什么,下面这张图,是代码随想录的一张回溯切割模拟图: |
---|
文章链接:https://leetcode.cn/problems/palindrome-partitioning/solutions/640336/131-fen-ge-hui-wen-chuan-hui-su-sou-suo-yp2jq/
- 可见这道题的示例就是实现,每次根据不同的个数进行切割。
- 每次切割的结果,为一种切割方案
- 题目要所有的方案
- 简单来说,我们选择一个start变量,作为这次切割的起始位置。例如"aab"这个字符串,对应下标位置为[0,1,2]
- 初始我们start肯定在0位置
- 我们第一次先切割1个. 也就是获取[0,0]位置的长度为1的字符"a"
- 此时,下一个位置只能从1开始,也就是start = 1.依然我们先选择切割1个,[1,1]位置取得"a". 再下个位置是2开始,start = 2,依然我们先切割一个[2,2]位置取得"b"
- 此时字符串切割完成,我们得到
一组切割方案[a,a,b]
- 然后,回头来看start = 2 ,我们现在选择切割2个[2,3],此时发现下标越界了,所以start = 2的情况枚举完成
- 回到start = 1的情况,选择切割2个[1,2],从而切割出"ab",发现不是回文串。然后发现字符串又枚举完了
- 回到start = 0的情况,选择切割2个[0,1]从而切割出"aa",发现是回文串
- 然后下次只能是start = 2.先切割1个[2,2]从而切割出"b",也是回文串。然后再次枚举完成
- 得到
第二个组切割方案[aa,b]
- 然后回到start = 0,选择切割3个[0,2],切割出"aab",不是回文串,字符串枚举完成,没有任何回文串。
- 故,得到两组切割方案[a,a,b]和[aa,b]
很显然,如果只要其中任意一种切割方案,都有很多方法解决,例如动态规划,记忆化搜索,双指针判断是否回文串等
但是这道题是要所有方案,所以,在上面这些方法的基础上,还得加上回溯。
1. 回溯+双指针
解题思路:时间复杂度O(
n
2
∗
l
o
g
2
n
n^2*log_2{n}
n2∗log2n),空间复杂度O(n^2) |
---|
- 使用start变量,指定当前想要枚举回文串的起点
- 然后以1,2,3…这样递增的数字,来限定本次以start为起点的字符串长度。
- 然后通过双指针法,判断是否是回文串,如果是,就添加到答案中
代码:此算法,使用双指针法,会产生大量重复计算,但是因为都是对数组进行直接操作,所以做题情况下,这个算法的速度最快,但是时间复杂度并不优秀 |
---|
class Solution {
List<List<String>> res;
String s;
int len;
public List<List<String>> partition(String s) {
this.res = new ArrayList<List<String>>();
this.s = s;
this.len = s.length();
String[] records = new String[len];
backtracking(0, records,0);
return res;
}
private void backtracking(int start, String[] records,int index) {
if (start == len) {
ArrayList<String> list = new ArrayList<>();
for(int i = 0;i<index;i++) list.add(records[i]);
res.add(list);
} else {
for (int i = start; i < len; i++) {
if(isPalin(s, start, i)) {
records[index] = s.substring(start,i+1);
backtracking( i + 1, records,index+1);
}
}
}
}
private boolean isPalin(String s, int low, int high) {
while (low < high) {
if (s.charAt(low) != s.charAt(high)) {
return false;
}
low++;
high--;
}
return true;
}
}
2. 动态规划+回溯
很多人觉得动态规划很难,但它就是固定套路而已。其实动态规划只不过是将多余的步骤,提前放到dp数组中(就是一个数组,只不过大家都叫它dp),达到空间换时间的效果。它仅仅只是一种优化思路,因此它目前的境地和线性代数一样----虚假的难。
- 想想线性代数,在国外留学的学生大多数不觉得线性代数难理解。但是中国的学生学习线性代数时,完全摸不着头脑,一上来就是行列式和矩阵,根本不知道这玩意是干嘛的。
- 线性代数从根本上是在空间上研究向量,抽象上研究线性关系的学科。人家国外的教科书都是第一讲就帮助大家理解研究向量和线性关系。
- 反观国内的教材,直接把行列式搞到第一章。搞的国内的学生在学习线性代数的时候,只会觉得一知半解,觉得麻烦,完全不知道这玩意学来干什么。当苦尽甘来终于理解线性代数时干什么的时候,发现人家国外的教材第一节就把这玩意讲清楚了。你只会大骂我们国内这些教材,什么狗东西(以上是自己学完线性代数后的吐槽,我们同学无一例外都这么觉得)。
而我想告诉你,动态规划和线性代数一样,我学完了才知道,它不过就是研究空间换时间,提前将固定的重复操作规划到dp数组中,而不用暴力求解,从而让效率极大提升。
- 但是网上教动态规划的兄弟们,你直接给一个动态方程是怎么回事?和线性代数,一上来就教行列式和矩阵一样,纯属恶心人。我差不多做了30多道动态规划题目,才理解,动态方程只是一个步骤而已,而这已经浪费我很长时间了,我每道题都一知半解不理解,过程及其痛苦。最后只能重新做。
- 动态规划,一定是优先考虑重复操作与dp数组之间的关系,搞清楚后,再提出动态方程。而你们前面步骤省略了不讲,一上来给个方程,不是纯属扯淡吗?
- 我推荐研究动态规划题目,按5个步骤,从上到下依次来分析
- DP数组及下标含义
- 递推公式
- dp数组初始化
- 数组遍历顺序(双重循环及以上时,才考虑)
- dp数组打印,分析思路是否正确(相当于做完题,检查一下)
- 上面的方法,每次都要重复判断某个范围的子串是否是回文串
- 例如,[a,a,b]和[aa,b]这两组方案,都重复的判断了"b"是否是回文串
- 现在我们将某个范围的子串是否是回文串保存到dp数组中。只需要处理一次dp数组,之后直接取值就知道是否是回文串了。
- 所以我们创建二维数组,横坐标表示子串起始位置,纵坐标表示子串结束位置。对应的元素值为当前这个子串是否是回文子串。
- DP数组及下标含义
- 我们
要求出的是
当前子串是否是回文串。显然dp数组中存储的是
当前子串是否是回文串。要求出谁的
?显然是求出,以横坐标为起点位置,纵坐标为终点位置的子串的。那么下标就是代表子串的左右边界
,很显然,需要两个下标,也就是二维数组。
- 递推公式
- 对于aba这个子串,我们判断两边的a,也就是(a)b(a),括号中内容是否相等。
- 如果相等,除去两边的,剩下的中间内容a(b)a,也就是b是否是回文串,若b是回文串,那么加上两边相同的a,aba就是回文串
- 引入两个下标i和j,i表示左边界,j表示右边界
- 先判断i和j位置的字符是否一致,如果一致,再判断去掉左右两边后,也就是i+1到j-1位置的子串是否是回文串
- 故递推公式为:dp[i][j] = s[i] == s[j] && dp[i+1][j-1]
- dp数组初始化:自低向上初始化,因为递推公式中,总是需要下一行的数据,也就是第i行需要dp[i+1]行的数据
- 数组遍历顺序:先行后列,i表示起始位置,然后j表示终止位置,先规定起始位置,再规定终止位置
- 打印dp数组(自己生成dp数组后,将dp数组输出看看,是否和自己预想的一样。),下图是"aab"这个字符串的dp数组
代码:时间复杂度O(
n
∗
2
n
n*2^n
n∗2n),空间复杂度O(n^2) |
---|
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);
}
}
}
}
3. 记忆化搜索+回溯
解题思路:就是动态规划方法,只不过是简化版动态规划。时间复杂度O(
n
∗
2
n
n*2^n
n∗2n),空间复杂度O(n^2) |
---|
- 不在需要抽象思考动态规划的dp数组初始化
- 而是改为,如果我们第一次遇到这个子串,就进行判断,如果它是回文串,就再对应dp[i][j]位置设置为true
- 之后如果再次遇到这个子串,直接取值,而不是再次判断这个子串是否是回文串
class Solution {
int[][] 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 int[n][n];
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 (isPalindrome(s, i, j) == 1) {
ans.add(s.substring(i, j + 1));
dfs(s, j + 1);
ans.remove(ans.size() - 1);
}
}
}
public int isPalindrome(String s, int i, int j) {
if (f[i][j] != 0) {
return f[i][j];
}
if (i >= j) {
f[i][j] = 1;
} else if (s.charAt(i) == s.charAt(j)) {
f[i][j] = isPalindrome(s, i + 1, j - 1);
} else {
f[i][j] = -1;
}
return f[i][j];
}
}