[回溯->动态规划]131. 132 分割回文串 I II (回溯法、动态规划)
131. 分割回文串 I(找出所有子串解集)
题目链接:https://leetcode-cn.com/problems/palindrome-partitioning/
分类:回溯法(分割子串)、动态规划(回文判断)、双指针法(回文判断)
思路1:回溯法(未做优化)
算法设计
1、变量设置
设置一个res作为最终结果集合,保存所有满足题意的子串列表,设置回溯函数中的传递参数start表示s剩余部分的起点,list用于存放当前构造中的子串列表。
2、回溯函数的设置:
状态变量:设置一个列表list保存分割的回文子串,设置一个start表示字符串剩余部分的起点,当depth==s.length()说明到达字符串末尾,没有剩余部分,就将当前构造的list加入最终结果集合res。
递归出口:start==s.length()时,说明已经划分到字符串末尾,不再有剩余字符可供划分,所以将list加入res.
递归主体:
首先明确子串的划分方式:例如"aabaa",可以划分为
1:a,a,b,a,a
2:a,a,b,aa
3:a,a,ba,a
4:a,a,baa
...
n:a,abaa 1,5
...
以此类推,划分方式可以总结为:最开始按一个字符的长度划分,直到递归最深处向上返回时,再取2个字符为子串的方式划分…,所以最小划分长度为1,最大划分长度为s.length() - 1,划分长度按步长=1递进,可以用:
for(int i = 1; i <= s.length(); i++) 来表示,
- 注意:在实现时使用的是substring划分子串进行回文判断,子串的起点就是start,终点是start+i,substring的特点是左闭右开,所以i的上界要取到s.length(),而不是s.length()-1。
每划分出一个新子串substring(start, start+i),就先判断该子串是不是回文子串:
- 如果是则加入list,进入下一层递归;
- 如果不是则跳过该子串。
进入下一层递归时,要记得更新下一层划分的起点为start+i,因为这一层的substring只取到了第start+i-1个字符,而没有取到第start+i个字符,所以就将这一层所处理的最后一个字符的下一个位置传递给下一层递归,作为下一层划分的起点。
从下一层递归返回时,list要移除最新加入的子串,恢复到进入递归前的状态。
3、子串回文的判断:双指针法
//判断字符串是否回文
public boolean isReverse(String str){
int left = 0, right = str.length() - 1;
while(left < right){
if(str.charAt(left) != str.charAt(right)) return false;
left++;
right--;
}
return true;
}
实现代码
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> partition(String s) {
if(s == null || s.length() == 0) return res;
List<String> list = new ArrayList<>();
backtrack(s, 0, 0, list);
return res;
}
//回溯递归函数
public void backtrack(String s, int depth, int start, List<String> list){
if(start == s.length()){
res.add(new ArrayList<>(list));
return;
}
else{
for(int i = 1; i <= s.length(); i++){//划分长度,可以从1~s.length
if(start + i <= s.length() && isReverse(s.substring(start, start + i))){
list.add(s.substring(start, start + i));
backtrack(s, depth + i, start + i, list);
list.remove(list.size() - 1);
}
}
}
}
//判断字符串是否回文
public boolean isReverse(String str){
int left = 0, right = str.length() - 1;
while(left < right){
if(str.charAt(left) != str.charAt(right)) return false;
left++;
right--;
}
return true;
}
}
思路1优化:动态规划提前构造回文判断数组
优化的切入点在于子串回文的判断,思路1采用的双指针法,但还有更快的方法,可以使用动态规划提前构造出一个二维数组标记字符串s的子串是否是回文。
状态设置:f(i,j)表示字符串s[i,j]是否回文
状态转移:
- 如果j - i < 2, dp[i][j] = s[i] == s[j]
- 如果j - i >= 2, dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1]
观察转移方程可以发现计算f(i,j)时要知道f(i+1,j-1),所以构造dp数组时要先构造j,再构造i(即外层for-j,内层for-i),才能在计算i,j时已经得出i+1,j-1.
实现遇到的问题:dp数组下标和substring下标的区间开闭情况不同
dp数组下标和substring下标的区间开闭情况不同,代码细节要做修改。
dp数组的i,j表示的是s的下标,是左右闭合区间,而substring是左闭右开,所以for-i选择划分长度是依据dp[i][j],所以for-i的上下界修改为从0~len-1,但截取子串substring时要取start,start+i+1。
同时,因为start+i作为这一层子串的终点包含在子串之内,所以下一层递归的起点要取最后一位的下一位,所以传递给下一层的参数是start+i+1。
实现代码
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> partition(String s) {
if(s == null || s.length() == 0) return res;
//预处理s,提前构造回文数组
boolean[][] dp = new boolean[s.length()][s.length()];
for(int j = 0; j < s.length(); j++){//右边界
for(int i = 0; i <= j; i++){//左边界
if(s.charAt(i) == s.charAt(j) && (j == i || (j - i) < 2 || dp[i + 1][j - 1]))
dp[i][j] = true;
}
}
List<String> list = new ArrayList<>();
backtrack(s, 0, 0, list, dp);
return res;
}
//回溯递归函数
public void backtrack(String s, int depth, int start, List<String> list, boolean[][] dp){
if(start == s.length()){
res.add(new ArrayList<>(list));
return;
}
else{
for(int i = 0; i < s.length(); i++){
if(start + i < s.length() && dp[start][start + i]){
list.add(s.substring(start, start + i + 1));
backtrack(s, depth + i, start + i + 1, list, dp);
list.remove(list.size() - 1);
}
}
}
}
}
132. 分割回文串 II(计算最少分割次数)
题目链接:https://leetcode-cn.com/problems/palindrome-partitioning-ii/
分类:动态规划(计算最少分割次数、回文判断)
题目分析:
和131题类似,但只需要计算最少分割次数,也就是用最少的分割次数就能得到都是回文的子串,如果按131的回溯法解题会导致超时,但问题可以用回溯法解题,自然问题也可以由大分解到小问题,从而用动态规划解题。
思路:动态规划
状态设置:f(i)表示s[0~i]所需的最少分割次数。
状态转移:(难点!!!)
- 如果s[0~i]本身就是回文串,则f(i)=0,表示不需要分割就已经是回文串;
- 如果s[0~i]不是回文串,则说明需要对s[0~i]进行分割,设分割点的下标是j,分割后的两部分为[0~j],[j+1~i],所以j的范围为:0~i-1:
- 首先明确,分割后的s[0~j]对应的f(j)表示s[0~j]所需的最少分割次数,所以可以认为s[0~j]经过f(j)次分割后已经得到了回文子串,接下来就是判断s[j+1~i]是不是回文的:
- 如果s[j+1~i]不是回文,则直接跳过当前分割点;
- 如果s[j+1~i]是回文,就把该分割点记为有效分割点。
- 在得到所有有效分割点之后,f(i)=所有有效分割点中的最少分割次数 + 1
- 首先明确,分割后的s[0~j]对应的f(j)表示s[0~j]所需的最少分割次数,所以可以认为s[0~j]经过f(j)次分割后已经得到了回文子串,接下来就是判断s[j+1~i]是不是回文的:
dp数组:
- 大小设置:dp[s.length()]
- 初始化:s[0]本身就是回文的,所以dp[0]=0
- 最终返回值:根据dp[i]表示的意义,最终结果保存在dp[s.length() - 1]
回文串的判断:使用5.最长回文子串(131.方法2)的动态规划方法提前构造回文数组reverse,得到该数组之后,判断某个子串[left,right]是不是回文,只需要判断reverse[left][right] == true?即可。
实现代码:
class Solution {
public int minCut(String s) {
if(s == null || s.length() < 2) return 0;
int len = s.length();
//构造回文判断数组
boolean[][] reverse = new boolean[len][len];
for(int right = 0; right < len; right++){
for(int left = 0; left <= right; left++){
if(s.charAt(left) == s.charAt(right) && ((right - left) < 2 || reverse[left + 1][right - 1]))
reverse[left][right] = true;
}
}
//分割子串的dp数组
int[] dp = new int[len];
dp[0] = 0;
for(int i = 1; i < len; i++){
if(reverse[0][i]) dp[i] = 0;
else{
int min = Integer.MAX_VALUE;
//从0开始寻找分割点
for(int j = 0; j < i; j++){
if(reverse[j + 1][i]) min = Math.min(dp[j], min);
}
dp[i] = min + 1;
}
}
return dp[len - 1];
}
}