class Solution {
int f(string &s, int L, int R){
if(L > R){
return 0;
}
if(L == R){
return 1;
}
if(s[L] == s[R]){
return f(s, L + 1, R - 1) + 2;
}
return std::max(f(s, L + 1, R), f(s, L, R - 1));
}
public:
int longestPalindromeSubseq(string s) {
return f(s, 0, (int)s.size() - 1);
}
};
会超时
(1)准备一个表。考虑递归函数的变化参数的个数和范围
int longPSQ(string &s, int l, int r)
所以dp数组应该是一个二维数组,如下:
int dp[N][N]
(2)返回值,看主函数是怎么调用的
return longPSQ(s, 0, (int)s.size() - 1);
所以应该返回dp[0][N - 1]
(3)填表
if(l > r){
return 0;
}
if(l == r){
return 1;
}
if(s[l] == s[r]){
return longPSQ(s, l + 1, r - 1) + 2;
}
return std::max(longPSQ(s, l + 1, r), longPSQ(s, l, r - 1));
(4)综上,代码如下:
class Solution {
public:
int longestPalindromeSubseq(string s) {
int N = s.size();
if(N == 0){
return 0;
}
std::vector<std::vector<int>> dp(N, std::vector<int>(N, 0));
for (int i = 0; i < N; ++i) {
dp[i][i] = 1;
}
for (int i = N - 1; i >= 0; --i) {
for (int j = i + 1; j < N; ++j) {
if(s[i] == s[j]){
dp[i][j] = dp[i + 1][j - 1] + 2;
}else{
dp[i][j] = std::max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][N - 1];
}
};
定义一个递归函数:
int f(std::string str, int L, int R);
其含义为:在str[L…R]区间中,返回其最长回文子序列长度
主函数怎么调用它:
return f(s, 0, (int)s.size() - 1);
怎么实现呢?
(1)base case
(2)一般情况
最终代码:
class Solution {
int f(string &str, int L, int R){
if(L == R){
return 1;
}
if(L == R - 1){
return str[L] == str[R] ? 2 : 1;
}
int p1 = f(str, L + 1, R - 1);
int p2 = f(str, L, R - 1);
int p3 = f(str, L + 1, R);
int p4 = str[L] != str[R] ? 0 : (2 + f(str, L + 1, R - 1));
return std::max(std::max(p1, p2), std::max(p3, p4));
}
public:
int longestPalindromeSubseq(string s) {
return f(s, 0, (int)s.size() - 1);
}
};
(1)准备一个表。考虑递归函数的变化参数的个数和范围
int f(std::string str, int L, int R);
所以dp数组应该是一个二维数组,如下:
int dp[N][N]
(2)返回值,看主函数是怎么调用的
return f(s, 0, (int)s.size() - 1);
所以应该返回dp[0][N - 1]
(3)填表
综上,代码为:
class Solution {
public:
int longestPalindromeSubseq(string s) {
int N = s.size();
if(N <= 1){
return N;
}
int dp[N][N];
// std::vector> dp(N, std::vector(N, 0));
dp[N - 1][N - 1] = 1;
for (int i = 0; i < N - 1; ++i) {
dp[i][i] = 1;
dp[i][i + 1] = s[i] == s[i + 1] ? 2 : 1;
}
for (int i = N - 3; i >= 0; --i) {
for (int j = i + 2; j < N; ++j) {
dp[i][j] = std::max( dp[i][j - 1], dp[i + 1][j]);
if(s[i] == s[j]){
dp[i][j] = std::max(dp[i][j], 2 + dp[i - 1][j + 1]);
}
}
}
return dp[0][N - 1];
}
};
之前我们已经解决了leetcode:1143. 最长公共子序列 Longest Common Subsequence ,而现在我们有一个str1,需要得到它的最长回文子串。
思路:已经知道str1,现在得到str1的逆序str2,然后得到str1和str2的最长公共子序列,这个最长公共子序列就是str1的最长回文子串
class Solution {
int longestCommonSubsequence(string &str1, string &str2){
int N = str1.size(), M = str2.size();
std::vector<std::vector<int>> dp(N, std::vector<int>(M, 0));
dp[0][0] = str1[0] == str2[0] ? 1 : 0;
for (int i = 1; i < N; i++) {
dp[i][0] = str1[i] == str2[0] ? 1 : dp[i - 1][0];
}
for (int j = 1; j < M; j++) {
dp[0][j] = str1[0] == str2[j] ? 1 : dp[0][j - 1];
}
for (int i = 1; i < N; i++) {
for (int j = 1; j < M; j++) {
dp[i][j] = std::max(dp[i - 1][j], dp[i][j - 1]);
if (str1[i] == str2[j]) {
dp[i][j] = std::max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
}
return dp[N - 1][M - 1];
}
public:
int longestPalindromeSubseq(string str) {
if(str.size() <= 1){
return str.size();
}
std::string rstr = str;
std::reverse(rstr.begin(), rstr.end());
return longestCommonSubsequence(str, rstr);
}
};
假设二维数组 dp[i][j] 记录子串 i…j 内的最长回文序列长度。
显然,任何一个子串的最长回文序列长度至少是 1, 即可初始化所有的 dp[i][j] = 1 。
考虑 dp 数组的递推关系。
如果子串的两边字符相等,那么去掉这俩字符后的子串的最长回文子序列长度比原来少了 2 。
即当 s[i] == s[j] 时,dp[i][j] = dp[i+1][j-1] + 2 。
如果两边字符不相等,最长回文序列要么全落在去掉右边界字符后的左子串内,要么全落在去掉左边界字符后的右子串内 。
此时 dp[i][j] = max(dp[i+1][j], dp[i][j-1]) 。
上面的两种递推关系,对于子串起始位置的变量 i 的利用逻辑是:先知道 i+1 时候的情况,才能知道 i 时候的情况。 所以 应倒序迭代变量 i ,同样的道理, 应正序迭代变量 j 。
需要注意处理边界情况:
对于第一种递推情况,要考虑子串 i+1…j-1 的有效性,即 i+1 <= j-1 。
对于第二种递推情况,利用的两个左右子串必然是有效的。
最后,要求的结果即 dp[0][n-1] ,其中 n 是字符串长度。
class Solution {
public:
int longestPalindromeSubseq(string s) {
if(s.empty()){
return 0;
}
int n = s.size();
// dp[i][j] 表示子串 i..j 内的最长回文序列长度
int dp[n][n];
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
// 初始化,至少为 1
dp[i][j] = 1;
if (s[i] == s[j]) {
// 第一种情况,两边字符相等 回文序列长度 += 2
// 注意子串i+1..j-1 的有效性
if (i + 1 <= j - 1)
dp[i][j] = dp[i + 1][j - 1] + 2;
else {
// 即 j-i <= 1 ,此时 i..j 至多有 2 个字符
// 两个字符相等时,自身回文,取其长度
dp[i][j] = j - i + 1;
}
} else {
// 第一种情况,两边字符不等 回文序列长度取左右之大
// 此时必然 j > i
// 所以,一定有 j >= i+1 或者 i-1 <= j,也就是子串一定有效
// 仍需要注意 i+1 和 j-1 的越界处理
if (i + 1 < n) dp[i][j] = max(dp[i][j], dp[i + 1][j]);
if (j - 1 >= 0) dp[i][j] = max(dp[i][j], dp[i][j - 1]);
}
}
}
return dp[0][n - 1];
}
};
最后,用一个表格图示来更好地理解其规划过程。
(1)确定状态
最后一步:
T[0] = T[M-1]
子问题:
S[i...j]
的最长回文子串状态:
(2)转移方程
if (s[i] == s[j])
// 它俩一定在最长回文子序列中
dp[i][j] = dp[i + 1][j - 1] + 2;
else
// s[i+1..j] 和 s[i..j-1] 谁的回文子序列更长?
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
(3)初始情况和边界情况
(4)遍历顺序
class Solution {
public:
int longestPalindromeSubseq(string s) {
int m = s.length();
if(m == 0){
return 0;
}
std::vector<std::vector<int>> dp(m,std::vector<int>(m));
// init
//length 1
for (int i = 0; i < m; ++i) {
dp[i][i] = 1;
}
//length 2
for (int i = 0; i < m - 1; ++i) {
if(s[i] == s[i + 1]){
dp[i][i + 1] = 2;
}else{
dp[i][i + 1] = 1;
}
}
for (int len = 0; len <= m; ++len) {
for (int i = 0; i + len <= m ; ++i) {
int j = i + len - 1;
char front = s[i], end = s[j];
if(s[i] == s[j]){
dp[i][j] = dp[i + 1][j - 1] + 2;
}else{
dp[i][j] = std::max(dp[i][j - 1], dp[i + 1][j]);
}
}
}
return dp[0][m - 1];
}
};
这是一道经典的区间dp题。之所以可以使用区间dp进行求解,是因为在给定一个回文串的基础上,如果在回文串的边缘分别添加两个新的字符,可以通过判断两字符是否相等来得知新串是否回文
也就是说,使用小区间的回文状态可以推导出大区间的回文状态值。
从图论上来看,任何一个长度为len的回文串,必然由[长度为len-1]或者[长度为len-2]的回文串转移而来
通常区间len问题都是: