给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1:
输入: “bbbab”
输出: 4
一个可能的最长回文子序列为 “bbbb”。
示例 2:
输入: “cbbd”
输出: 2
一个可能的最长回文子序列为 “bb”。
提示:
1 <= s.length <= 1000
s 只包含小写英文字母
话不多说,我先展示下我优化出来的动态规划最终版本在LeetCode上的执行情况。
再逐步从暴力递归改到终极版本,希望大家能对暴力递归改动态规划有个比较清楚的认识。(任何暴力递归都可以改成动态规划,但动态规划不是太好写,暴力递归却比较接近自然智慧,比较好些出来)
(1) 可能性1: 字符串arr[R]位置的字符没有包含在 最长回文子序列中,即arr[R]与最长子序列无关
例如: s = “b12321ba” 最长回文子序列为 b12321b,有没有最后一个字符a,其结果都一样
(2) 可能性2: 字符串arr[L]位置的字符没有包含在 最长回文子序列中,即arr[L]与最长子序列无关
例如: s = “ab123321b” 最长回文子序列为 b123321b,有没有第一个字符a,其结果都一样
(3) 可能性3: 字符串arr[L]、arr[R]位置的字符没有包含在 最长回文子序列中,即arr[L]、arr[R]与最长子序列无关
例如: s = “ab123321bs” 最长回文子序列为 b123321b,有没有第一个字符a,和最后一个字符s,其结果都一样
(4)可能性 4: 字符串arr[L]、arr[R]位置的字符都包含在 最长回文子序列中,即arr[L]、arr[R]与最长子序列都有关
例如: s = “ac123f32d1ba” 最长回文子序列为 a123f321a, 第一个字符是a,最后一个字符也是a,其结果应该是去除第一个字符和最后一个字符形成字符串的最长回文子序列的长度+2 (第一个字符和最后一个字符的长度一共为2), 即可能性3的结果 +2
public static int palindromeSubsequence1(String s){
if (s == null || s.length() == 0){
return 0;
}
char[] str = s.toCharArray();
return process1(str,0,str.length-1);
}
public static int process1(char[] str,int L,int R){
if (L == R){ // 字符串中只有一个字符,则该字符串的回文子序列最大长度必定是1
return 1;
}
if (L + 1 == R){
// 字符串中还剩下两个字符,如果两个字符相等,则最长子序列的长度为2,
// 否则,长度为1,即两个字符中的任意一个都是最长子序列中的字符
return (str[L] == str[R] ? 2 : 1);
}
//可能性1:字符串arr[R]位置的字符没有包含在 最长子序列中,即arr[R]与最长子序列无关
int a1 = process1(str,L,R-1);
//可能性2:字符串arr[L]位置的字符没有包含在 最长子序列中,即arr[L]与最长子序列无关
int a2 = process1(str,L+1,R);
//可能性3:字符串arr[L]、arr[R]位置的字符没有包含在 最长子序列中,即arr[L]、arr[R]与最长子序列无关
int a3 = process1(str,L+1,R-1);
//可能性4:字符串arr[L]、arr[R]元素包含在最长回文子序列中,其长度为可能性3的结果+2(需要算上arr[L]、arr[R]两个元素)
int a4 = (str[L] == str[R] ? 2 + process1(str,L+1,R-1) : 0);
return Math.max(Math.max(a1,a2),Math.max(a3,a4));
}
动态规划版本1:
public static int palindromeSubsequence2(String s){
if (s == null || s.length() == 0){
return 0;
}
char[] str = s.toCharArray();
int N = str.length;
int[][] dp = new int[N][N];
dp[N-1][N-1] = 1;
for (int i = 0; i < N-1; i++) {
dp[i][i] = 1;
//对应暴力方法中的 if (L == R){ return 1; },字符长度为1
dp[i][i+1] = (str[i] == str[i+1] ? 2 : 1);
//if (L + 1 == R){ return (str[L] == str[R] ? 2 : 1);} 有两个字符
}
for (int L = N-3; L >= 0; L--){
for (int R = L + 2; R < N; R++) {
int p1 = dp[L][R-1];
//可能性1: int a1 = process1(str,L,R-1);
int p2 = dp[L+1][R];
//可能性2: int a2 = process1(str,L+1,R);
int p3 = dp[L+1][R-1];
//可能性3: int a3 = process1(str,L+1,R-1);
int p4 = (str[L] == str[R] ? 2 + dp[L +1][R-1] : 0);
//可能性4: int a4 = (str[L] == str[R] ? 2 + process1(str,L+1,R-1) : 0);
dp[L][R] = Math.max(Math.max(p1,p2),Math.max(p3,p4));
}
}
return dp[0][N-1];
// 对应暴力方法中的 return process1(str,0,str.length-1);
}
在dp表中:
因为c位置依赖的是b,e,f, c是b,e,f三者中的最大者
b位置依赖的是a,d,e, b位置的元素是a,d,e中的最大者,
f位置依赖的是e,h,k ,f是e,h,k中的最大者
所以,b、f一定大于e, 可能性三可以不予考虑(arr[L]、arr[R]与最长子序列无关)
public static int palindromeSubsequence3(String s){
if (s == null || s.length() == 0){
return 0;
}
char[] str = s.toCharArray();
int N = str.length;
int[][] dp = new int[N][N];
dp[N-1][N-1] = 1;
for (int i = 0; i < N-1; i++) {
dp[i][i] = 1;
dp[i][i+1] = (str[i] == str[i+1] ? 2 :1);
}
// 在写循环的时候,要一定注意表中元素之间的依赖关系,依赖关系决定填表顺序
for (int L = N - 3;L >= 0;L--){
for (int R = L + 2;R < N;R++){
int max = Math.max(dp[L][R-1],dp[L+1][R]);
if (str[L] == str[R]){
max = Math.max(max,2 + dp[L +1][R-1]);
}
dp[L][R] = max;
}
}
return dp[0][N-1];
}
要点总结:
1.梳理好问题存在的可能性是写好暴力递归的关键。
2.注意退出递归的条件,避免出现死循环的现象
在最长回文子序列问题中,退出递归的条件是 只有一个字符、只有两个字符的情况
if (L == R){ return 1; }
if (L + 1 == R){ return (str[L] == str[R] ? 2 : 1); }
3. 在写循环的时候,要一定注意表中元素之间的依赖关系,依赖关系决定填表顺序