4.回文子序列
最长回文子序列
题目链接 :516. Longest Palindromic Subsequence
题解链接:畅游面试中的动态规划套路-回文子序列系列之最长回文子序列
方法1:暴力递归
helper(String s, int start, int end)
函数表示,从start
索引到end
索引,所能找到的当前s
的最长回文子序列的长度base case
:start == end
当前单词只有一个字符,长度为1start > end
不合法
public int longestPalindromeSubseq(String s) {
return helper(s, 0, s.length() - 1);
}
private int helper(String s, int start, int end) {
if (start == end) return 1;
if (start > end) return 0;
int ans = 0;
if (s.charAt(start) == s.charAt(end)) {
ans = helper(s, start + 1, end - 1) + 2;
} else {
ans = Math.max(helper(s, start + 1, end),
helper(s, start, end - 1));
}
return ans;
}
方法2:自顶向下记忆化递归(Top-down)
对方法1进行记忆化修改后可以得到方法2
- 准备一个
h
e
l
p
e
r
(
S
t
r
i
n
g
s
,
i
n
t
i
,
i
n
t
j
)
helper(String s, int i, int j)
helper(Strings,inti,intj)函数,其中
s
是字符串本身,i
与j
是起始位置,memo
记录的是字符的最长子序列长度, m e m o [ 0 ] [ n − 1 ] memo[0][n-1] memo[0][n−1]为所求 - 记忆化:当
memo[i][j]
不为null
的时候,说明不是初始化的值,被求解过,直接返回 - 出口条件:
- 当
i>j
时,返回0
,因为没有意义,我们要求的起始位置i
要小于j
的 - 当
i==j
时,返回1
,只有一个字符,可以形成回文,即是其本身,长度为1
- 当
- 递归逻辑:
- 当
s[i]==s[j]
时,说明需要在 h e l p e r ( i + 1 , j − 1 ) helper(i+1,j-1) helper(i+1,j−1)基础上+2
- 当
s[i]!=s[j]
时,要取 m a x [ h e l p e r ( i + 1 , j ) , h e l p e r ( i , j − 1 ) ] max[helper(i+1,j),helper(i,j-1)] max[helper(i+1,j),helper(i,j−1)]
- 当
Integer[][] memo;
public int longestPalindromeSubseq(String s) {
memo = new Integer[s.length()][s.length()];
return helper(s, 0, s.length() - 1);
}
private int helper(String s, int start, int end) {
if(memo[start][end]!=null) return memo[start][end];
if (start == end) return 1;
if (start > end) return 0;
int ans = 0;
if (s.charAt(start) == s.charAt(end)) {
ans = helper(s, start + 1, end - 1) + 2;
} else {
ans = Math.max(helper(s, start + 1, end),
helper(s, start, end - 1));
}
return memo[start][end] = ans ;
}
方法3:自底向上填表DP(Bottom-up)
- 定义
dp[i][j]
:表示s[i...j]
之间的最长子序列的长度,注意是子序列,不是子串,子序列是可以跳跃的,子串不可以- 当
s[i]==s[j]
时,说明i
与j
位置的字符可以形成一个回文,这个回文的长度为2
,根据dp
的思想,其结果应该是依赖前面的结果,也就是s[i+1 .... j-1]
这个范围的字符回文个数,也就是dp[i+1][j-1]
,即 d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ] + 2 dp[i][j]=dp[i+1][j-1]+2 dp[i][j]=dp[i+1][j−1]+2 - 当
s[i]!=s[j]
时,说明i
与j
位置的字符不能形成一个回文,这个时候要看 s [ i + 1... j ] s[i+1...j] s[i+1...j]与 s [ i . . . j − 1 ] s[i...j-1] s[i...j−1]这两段,因为 s [ i + 1 ] s[i+1] s[i+1]可能与 s [ i + 2... j ] s[i+2...j] s[i+2...j]范围内的某个字符相同,拼凑出回文,因为 s [ i ] ! = s [ j ] s[i]!=s[j] s[i]!=s[j],同理可得 s [ i . . . j − 1 ] s[i...j-1] s[i...j−1]这段,故此, d p [ i ] [ j ] = m a x [ d p [ i ] [ j − 1 ] , d p [ i + 1 ] [ j ] ] dp[i][j]=max[dp[i][j-1],dp[i+1][j]] dp[i][j]=max[dp[i][j−1],dp[i+1][j]]
- 当
base case
:- 很容想到的是
i==j
时,说明s[i...j]
只有一个字符,此时其自身可以形成一个回文,长度为1
- 当
i>j
时,此时是不存在的,因为我们规定了s[i...j]
起始位置i
要小于结束位置j
的,此时初始化为0
- 很容想到的是
- 有两种遍历方式
- 斜着遍历
- 倒着遍历
- 返回结果 d p [ 0 ] [ n − 1 ] dp[0][n-1] dp[0][n−1]其实就是 s [ 0... n − 1 ] s[0...n-1] s[0...n−1]的最长回文子序列的长度
public int longestPalindromeSubseq1st(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for (int i = n - 1; i >= 0; i--) {
dp[i][i] = 1;
for (int j = i + 1; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][n - 1];
}
最长回文子串
题目链接 :5. Longest Palindromic Substring
题解链接:畅游面试中的动态规划套路-回文子序列系列之最长回文子串
方法1:暴力递归
-
helper(String s, int start, int end)
- 表示
s
从start
到end
位置,是否有回文子串
- 表示
-
base case
:start == end
: 相等时,说明只有一个字符了,返回T
start +1== end
:两个字符的时候,比较两个字符是否相等
public String longestPalindrome(String s) {
String ans = "";
for (int i = 0; i < s.length(); i++) {
for (int j = i; j < s.length(); j++) {
if (helper(s, i, j) && j - i + 1 > ans.length()) {
ans = s.substring(i, j + 1);
}
}
}
return ans;
}
private boolean helper(String s, int start, int end) {
if (start == end) return true;
if (start + 1 == end) return s.charAt(start) == s.charAt(end);
boolean ans = false;
if (s.charAt(start) == s.charAt(end)) {
ans = helper(s, start + 1, end - 1);
}
return ans;
}
方法2:自顶向下记忆化递归(Top-down)
脱胎与方法1,添加记忆化
Boolean[][] memo;
public String longestPalindrome(String s) {
memo = new Boolean[s.length()][s.length()];
String ans = "";
for (int i = 0; i < s.length(); i++) {
for (int j = i; j < s.length(); j++) {
if (helper(s, i, j) && j - i + 1 > ans.length()) {
ans = s.substring(i, j + 1);
}
}
}
return ans;
}
private boolean helper(String s, int start, int end) {
if (start == end) return true;
if (start + 1 == end) return s.charAt(start) == s.charAt(end);
if (memo[start][end] != null) return memo[start][end];
boolean ans = false;
if (s.charAt(start) == s.charAt(end)) {
ans = helper(s, start + 1, end - 1);
}
return memo[start][end] = ans;
}
方法3:自底向上填表递归(Bottom-up)
-
其中
f[i][j]
表示s
中,从i
到j
是否有回文子串 -
k
为遍历的字符长度,可以为n
- 此时
i=0
,j=i+k-1=0+n-1=n-1
- 此时
-
条件为当前字符
[i]==[j]
的时候,要么只有两个字符,要么砍头去尾,有回文子串
public String longestPalindrome(String s) {
if (s == null || s.length() == 0) return "";
int n = s.length();
boolean[][] f = new boolean[n][n];
for (int i = 0; i < n; ++i) f[i][i] = true;
int maxLen = 1, start = 0;
for (int k = 2; k <= n; k++) {
// System.out.printf("k:%d\n", k);
for (int i = 0; i < n - k + 1; i++) {
int j = i + k - 1;
// System.out.printf("i:%d,j:%d\n", i, j);
if (s.charAt(i) == s.charAt(j) && (k == 2 || f[i + 1][j - 1])) {
f[i][j] = true;
if (maxLen < k) {
maxLen = k;
start = i;
}
}
}
}
return s.substring(start, start + maxLen);
}
另外一种写法
k
为遍历的字符长度,可以为n
即当i=0
的时候
public String longestPalindrome(String s) {
if (s == null || s.length() <= 0) return s;
int n = s.length();
boolean[][] f = new boolean[n][n];
for (int i = 0; i < n; i++) f[i][i] = true;
int maxLen = 1, start = 0;
for (int i = n - 1; i >= 0; i--) {
for (int k = 1; k < n - i; k++) {
int j = k + i;
if (s.charAt(i) == s.charAt(j)) {
f[i][j] = (k == 1) || f[i + 1][j - 1];
}
if (f[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
start = i;
}
}
}
return s.substring(start, start + maxLen);
}
方法4:中心扩展法
public String longestPalindrome(String s) {
if (s == null || s.length() == 0) {
return "";
}
int n = s.length();
int start = 0, end = 0;
for (int i = 0; i < n; i++) {
//获取到当前点i 的奇回文和偶回文的最大长度
int len1 = expandBySeed(s, i, i);
int len2 = expandBySeed(s, i, i + 1);
//取最大长度,然后扩展
int len = Math.max(len1, len2);
if (len > (end - start)) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
/**
* 由中心往两边扩散,返回满足最大回文的长度
*
* @param s
* @param start
* @param end
* @return
*/
private int expandBySeed(String s, int start, int end) {
int n = s.length();
while (start >= 0 && end < n && s.charAt(start) == s.charAt(end)) {
start--;
end++;
}
return end - start - 1;
}
方法5:Manacher算法
本动态规划的文章着重讲动态规划,涉及马拉车算法的内容不详细展开,下面的代码取自weiwei大佬的题解
public class Solution {
public String longestPalindrome(String s) {
// 特判
int len = s.length();
if (len < 2) {
return s;
}
// 得到预处理字符串
String str = addBoundaries(s, '#');
// 新字符串的长度
int sLen = 2 * len + 1;
// 数组 p 记录了扫描过的回文子串的信息
int[] p = new int[sLen];
// 双指针,它们是一一对应的,须同时更新
int maxRight = 0;
int center = 0;
// 当前遍历的中心最大扩散步数,其值等于原始字符串的最长回文子串的长度
int maxLen = 1;
// 原始字符串的最长回文子串的起始位置,与 maxLen 必须同时更新
int start = 0;
for (int i = 0; i < sLen; i++) {
if (i < maxRight) {
int mirror = 2 * center - i;
// 这一行代码是 Manacher 算法的关键所在,要结合图形来理解
p[i] = Math.min(maxRight - i, p[mirror]);
}
// 下一次尝试扩散的左右起点,能扩散的步数直接加到 p[i] 中
int left = i - (1 + p[i]);
int right = i + (1 + p[i]);
// left >= 0 && right < sLen 保证不越界
// str.charAt(left) == str.charAt(right) 表示可以扩散 1 次
while (left >= 0 && right < sLen && str.charAt(left) == str.charAt(right)) {
p[i]++;
left--;
right++;
}
// 根据 maxRight 的定义,它是遍历过的 i 的 i + p[i] 的最大者
// 如果 maxRight 的值越大,进入上面 i < maxRight 的判断的可能性就越大,这样就可以重复利用之前判断过的回文信息了
if (i + p[i] > maxRight) {
// maxRight 和 center 需要同时更新
maxRight = i + p[i];
center = i;
}
if (p[i] > maxLen) {
// 记录最长回文子串的长度和相应它在原始字符串中的起点
maxLen = p[i];
start = (i - maxLen) / 2;
}
}
return s.substring(start, start + maxLen);
}
/**
* 创建预处理字符串
*
* @param s 原始字符串
* @param divide 分隔字符
* @return 使用分隔字符处理以后得到的字符串
*/
private String addBoundaries(String s, char divide) {
int len = s.length();
if (len == 0) {
return "";
}
if (s.indexOf(divide) != -1) {
throw new IllegalArgumentException("参数错误,您传递的分割字符,在输入字符串中存在!");
}
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < len; i++) {
stringBuilder.append(divide);
stringBuilder.append(s.charAt(i));
}
stringBuilder.append(divide);
return stringBuilder.toString();
}
}
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
回文子串的个数
题目链接 :647. Palindromic Substrings
题解链接:畅游面试中的动态规划套路-回文子序列系列之回文子串的个数
方法1:暴力递归
public int countSubstrings(String s) {
int ans = 0, n = s.length();
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
ans += helper(s, i, j);
}
}
return ans;
}
private int helper(String s, int start, int end) {
if (start >= end) return 1;
return s.charAt(start) == s.charAt(end) ? helper(s, start + 1, end - 1) : 0;
}
方法2:自顶向下记忆化递归(Top-down)
Integer[][] memo;
public int countSubstrings(String s) {
int ans = 0, n = s.length();
memo = new Integer[n][n];
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
ans += helper(s, i, j);
}
}
return ans;
}
private int helper(String s, int start, int end) {
if (start >= end) return 1;
if (memo[start][end] != null) return memo[start][end];
return memo[start][end] = s.charAt(start) == s.charAt(end) ? helper(s, start + 1, end - 1) : 0;
}
方法3:自底向上填表DP(Bottom-up)
f[i][j]
表示i
到j
上的回文子串的个数,分三种情况讨论- 长度为1:自己和自己组成回文,回文子串个数为1
- 长度为2:是否相等,相等则有一个回文子串,不相等则没有
- 长度为3:一般情况,看是否相等,不相等,则为0,相等,则砍头去尾,看
f[i + 1][j - 1]
public int countSubstrings(String s) {
int n = s.length();
int[][] f = new int[n][n];
char[] chas = s.toCharArray();
int ans = 0;
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
if (i == j) f[i][j] = 1;
else if (i + 1 == j) f[i][j] = chas[i] == chas[j] ? 1 : 0;
else f[i][j] = chas[i] == chas[j] ? f[i + 1][j - 1] : 0;
ans += f[i][j];
}
}
return ans;
}
方法4:中心扩展法
/**
* @param s
* @return
*/
public int countSubstrings2nd(String s) {
int result = 0;
for (int i = 0; i < s.length(); i++) {
//以当前点i位置,向两边扩展,以i i+1位置向两边扩展
result += countSegment(s, i, i);
result += countSegment(s, i, i + 1);
}
return result;
}
public int countSegment(String s, int start, int end) {
int count = 0;
//start往左边跑,end往右边跑,注意边界
while (start >= 0 && end < s.length() && s.charAt(start--) == s.charAt(end++)) {
count++;
}
return count;
}
让字符串成为回文串的最少插入次数
题目链接:1312. Minimum Insertion Steps to Make a String Palindrome
老题解链接:动态规划解最长子序列子串等一类问题之让字符串成为回文及其Follow Up[Sika Deer]
新题解链接:畅游面试中的动态规划套路-回文子序列系列之让字符串成为回文串的最少插入次数
方法1:暴力递归
helper(String s, int start, int end)
表示s
从start
到end
想要形成回文,最少的插入次数base case
:- 当
start >= end
时,一个字符的时候,本身就是回文,不需要添加,大于的时候,越界,无意义
- 当
[start]=[end]
的时候,砍头去尾向前看[start]!=[end]
的时候,前后各添加一个,进入递归找最小的
public int minInsertions(String s) {
return helper(s, 0, s.length() - 1);
}
private int helper(String s, int start, int end) {
if (start >= end) return 0;
int ans;
if (s.charAt(start) == s.charAt(end)) {
ans = helper(s, start + 1, end - 1);
} else {
ans = Math.min(helper(s, start, end - 1), helper(s, start + 1, end)) + 1;
}
return ans;
}
方法2:自顶向下记忆化递归(Top-down)
Integer[][] memo;
public int minInsertions(String s) {
int n = s.length();
memo = new Integer[n][n];
return helper(s, 0, n - 1);
}
private int helper(String s, int start, int end) {
if (memo[start][end] != null) return memo[start][end];
if (start >= end) return 0;
int ans;
if (s.charAt(start) == s.charAt(end)) {
ans = helper(s, start + 1, end - 1);
} else {
ans = Math.min(helper(s, start, end - 1), helper(s, start + 1, end)) + 1;
}
return memo[start][end] = ans;
}
方法3:自底向上填表DP(Bottom-up)
public int minInsertions(String s) {
// $dp[i][j]$表示子串$str[i...j]$范围内的最少添加多少个字符后,可以形成回文子串
char[] chas = s.toCharArray();
int n = chas.length;
int[][] dp = new int[n][n];
for (int j = 1; j < n; j++) {
dp[j - 1][j] = (chas[j - 1] == chas[j]) ? 0 : 1;
for (int i = j - 2; i >= 0; i--) {
if (chas[i] == chas[j]) dp[i][j] = dp[i + 1][j - 1];
else dp[i][j] = Math.min(dp[i + 1][j], dp[i][j - 1]) + 1;
}
}
return dp[0][s.length() - 1];
}
另外一种解法
使用畅游面试中的动态规划套路-回文子序列系列之最长回文子序列递减即可
public int minDelBuildPalindrome(String s) {
int n = s.length();
int[][] f = new int[n][n];
for (int i = n - 1; i >= 0; i--) {//i的顺序从高到低
f[i][i] = 1;
for (int j = i + 1; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) f[i][j] = f[i + 1][j - 1] + 2;
else f[i][j] = Math.max(f[i + 1][j], f[i][j - 1]);
}
}
return n - f[0][n - 1];
}
怎么删掉最少字符构成回文
题目链接:Minimum Deletions in a String to make it a Palindrome
题解链接:畅游面试中的动态规划套路-回文子序列系列之怎么删掉最少字符构成回文
这题和畅游面试中的动态规划套路-回文子序列系列之最长回文子序列一样,相当于求最长回文子序列,相减可得 s.length()-maxLPSLen
方法1:暴力递归
public int minDelBuildPalindrome(String s) {
return s.length() - helper(s, 0, s.length() - 1);
}
private int helper(String s, int start, int end) {
if (start == end) return 1;
if (start > end) return 0;
int ans;
if (s.charAt(start) == s.charAt(end)) {
ans = helper(s, start + 1, end - 1) + 2;
} else {
ans = Math.max(helper(s, start, end - 1), helper(s, start + 1, end));
}
// System.out.println(ans);
return ans;
}
方法2:自顶向下记忆化递归(Top-down)
Integer[][] memo ;
public int minDelBuildPalindrome(String s) {
memo = new Integer[s.length()][s.length()];
return s.length() - helper(s, 0, s.length() - 1);
}
private int helper(String s, int start, int end) {
if(memo[start][end]!=null) return memo[start][end];
if (start == end) return 1;
if (start > end) return 0;
int ans;
if (s.charAt(start) == s.charAt(end)) {
ans = helper(s, start + 1, end - 1) + 2;
} else {
ans = Math.max(helper(s, start, end - 1), helper(s, start + 1, end));
}
// System.out.println(ans);
return memo[start][end]=ans;
}
方法3:自底向上填表DP(Bottom-up)
public int minDelBuildPalindrome(String s) {
int n = s.length();
int[][] f = new int[n][n];
for (int i = n - 1; i >= 0; i--) {//i的顺序从高到低
f[i][i] = 1;
for (int j = i + 1; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) f[i][j] = f[i + 1][j - 1] + 2;
else f[i][j] = Math.max(f[i + 1][j], f[i][j - 1]);
}
}
return n - f[0][n - 1];
}
分割回文串
题目链接 :131. Palindrome Partitioning
题解链接:畅游面试中的动态规划套路-回文子序列系列之分割回文串
方法1:暴力递归
dfs(String s)
返回当前字符s
所能分割的回文串的组合- 先判断左部分是不是回文,不是回文跳过,是回文,再递归去拿右部分的回文串组合
- 出口条件是当前的字符串为空的时候,也就是分割结束
public List<List<String>> partition(String s) {
return dfs(s);
}
private List<List<String>> dfs(String s) {
List<List<String>> res = new ArrayList<>();
if (s == null || s.length() == 0) res.add(new ArrayList<>());
for (int i = 0; i < s.length(); i++) {
if (isPalindrome(s, 0, i)) {
String left = s.substring(0, i + 1);
for (List<String> rightList : dfs(s.substring(i + 1))) {
List<String> sub = new ArrayList<>();
sub.add(left);
sub.addAll(rightList);
res.add(sub);
}
}
}
return res;
}
private boolean isPalindrome(String s, int l, int r) {
while (l < r) if (s.charAt(l++) != s.charAt(r--)) return false;
return true;
}
- 另外一种写法
List<List<String>> res = new ArrayList<>();
public List<List<String>> partition(String s) {
dfs(s, 0, s.length(), new ArrayList<>());
return res;
}
private void dfs(String s, int curr, int total, ArrayList<String> sub) {
if (curr == total) {
res.add(new ArrayList<>(sub));
return;
}
for (int i = curr; i < total; i++) {
if (isPalindrome(s, curr, i)) {
sub.add(s.substring(curr, i + 1));
dfs(s, i + 1, total, sub);
sub.remove(sub.size() - 1);
}
}
}
private boolean isPalindrome(String s, int l, int r) {
while (l < r) if (s.charAt(l++) != s.charAt(r--)) return false;
return true;
}
方法2:自顶向下记忆化递归(Top-down)
脱胎于方法1
Map<String, List<List<String>>> memo = new HashMap<>();
public List<List<String>> partition(String s) {
List<List<String>> dfs = dfs(s);
return dfs;
}
private List<List<String>> dfs(String s) {
if (memo.containsKey(s)) return memo.get(s);
List<List<String>> res = new ArrayList<>();
if (s == null || s.length() == 0) res.add(new ArrayList<>());
for (int i = 0; i < s.length(); i++) {
if (isPalindrome(s, 0, i)) {
String left = s.substring(0, i + 1);
//右部分从i+1开始
for (List<String> rightList : dfs(s.substring(i + 1))) {
List<String> sub = new ArrayList<>();
sub.add(left);
sub.addAll(rightList);
res.add(sub);
}
}
}
memo.put(s, res);
return res;
}
private boolean isPalindrome(String s, int l, int r) {
while (l < r) if (s.charAt(l++) != s.charAt(r--)) return false;
return true;
}
方法3:自底向上填表DP(Bottom-up)
dp[i][j]
表示[i...j]
范围内子串能否形成回文,先初始化该dp
表
List<List<String>> res = new ArrayList<>();
public List<List<String>> partition(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n];
for (int j = 0; j < n; j++) {
for (int i = 0; i <= j; i++) {
if (s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1])) dp[i][j] = true;
}
}
dfs(s, 0, n, new ArrayList<>(), dp);
return res;
}
private void dfs(String s, int i, int n, List<String> sub, boolean[][] dp) {
if (i == n) {
res.add(new ArrayList<>(sub));
return;
}
for (int j = i; j < n; j++) {
if (dp[i][j]) {
sub.add(s.substring(i, j + 1));
dfs(s, j + 1, n, sub, dp);
sub.remove(sub.size() - 1);
}
}
}