问题描述
给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000。
示例 1:
输入:
"bbbab"
输出:
4
一个可能的最长回文子序列为 "bbbb"。
示例 2:
输入:
"cbbd"
输出:
2
一个可能的最长回文子序列为 "bb"。
分析
对任意字符串,如果头和尾相同,那么它的最长回文子序列一定是去头去尾之后的部分的最长回文子序列加上头和尾。如果头和尾不同,那么它的最长回文子序列是去头的部分的最长回文子序列和去尾的部分的最长回文子序列的较长的那一个。
str[0...n-1]是给定的字符串序列,长度为n,假设f(0,n-1)表示序列str[0...n-1]的最长回文子序列的长度。
1.如果str的最后一个元素和第一个元素是相同的,则有:f(0,n-1)=f(1,n-2)+2;例如字符串序列“AABACACBA”;第一个元素和最后一个元素相同,其中f(1,n-2)表示红色部分的最长回文子序列的长度;
2.如果str的最后一个元素和第一个元素是不相同的,则有:f(0,n-1)=max(f(1,n-1),f(0,n-2));例如字符串序列“ABACACB”,其中f(1,n-1)表示去掉第一元素的子序列,f(0,n-2)表示去掉最后一个元素的子序列。
设字符串为s,f(i,j)表示s[i..j]的最长回文子序列。
状态转移方程如下:
当i>j时,f(i,j)=0。
当i=j时,f(i,j)=1。
当i<j并且s[i]=s[j]时,f(i,j)=f(i+1,j-1)+2。
当i<j并且s[i]≠s[j]时,f(i,j)=max( f(i,j-1), f(i+1,j) )。
3. 以"BBABCBCAB"为例,备忘录如下
4. 依据填表方向,可以分为两种方式迭代。
从左上角向右下方向填写:
对应遍历方向第一层为为从左至右,第二层从右至左;第一个位置上的B与第二个位置上的B比较,然后第二个位置上的B与第三个位置上的A比较,接着是第一个位置上的B与第三个位置上的A比较,继续。。。
从右下角向左上方向填写:
对应遍历方向第一层是从右至左,第二层方向从左至右;倒数第二个位置上的A与倒数第一个位置上的B比较,然后是倒数第三个位置的C和倒数第二个位置的A比较,接着是倒数第三个位置的C和倒数第一个位置的B比较,继续。。。
为什么是这个顺序,因为某个点依赖其子问题结果的数据分别是其左侧、下方、左下。所以迭代前必须确保其数据先计算。
代码
正序版(空间复杂度O(n^2))
class Solution {
public:
int longestPalindromeSubseq(string s) {
if (s.empty())
return 0;
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int j = 1; j < n; j++)
dp[i][i] = 1;
for (int i = j-1; i >=0; i--) {
if (s[i] == s[j])
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
return dp[0][n - 1];
}
};
倒序版(空间复杂度O(n^2))
class Solution {
public:
int longestPalindromeSubseq(string s) {
if (s.empty())
return 0;
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for(int i = n - 1; i >= 0; i--)
{
dp[i][i] = 1;
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] = max(dp[i][j-1],dp[i+1][j]);
}
}
return dp[0][n-1];
}
};
倒序版(空间复杂度O(2n))
分析下上面的代码以及填表时需要的数据。发现计算当前行时,只需要当前行以及下一行数据,以及当前列与左边的列。这里完全可以复用,用两行即可。
class Solution {
public:
int longestPalindromeSubseq(string s) {
if (s.empty())
return 0;
int n = s.size();
vector<vector<int>> dp(2, vector<int>(n, 0));
int cur = 0;
for(int i = n - 1; i >= 0; i--)
{
cur ^= 1;
dp[cur][i] = 1;
for(int j = i + 1; j < n; j++)
{
if(s[i] == s[j])
dp[cur][j] = dp[cur^1][j-1] + 2;
else
dp[cur][j] = max(dp[cur][j-1],dp[cur^1][j]);
}
}
return dp[cur][n-1];
}
};
倒序版(空间复杂度O(n))
到目前为止,空间复杂度降至2n,能不能再优化。
事实上可以压缩至n。
1. 初始化dp,即图中的橘色部分。
2. 从右下方向左上方迭代。
第一次对应的值为i=7,j=8,该值依赖初始的dp[7]和dp[8],以及初始值0。完成更新后填入dp[8],即更新后的dp[8];接下来i值递减,j从7开始,dp[7]依赖初始化的dp[6]和dp[7]以及初始值0,完成更新后即时新的dp[7]。j递增,dp[8]依赖更新后的dp[7]以及上一轮更新后的dp[8]还有上一轮未更新的初始化的dp[7]。继续。。。
3. 按照描述,代码如下:
class Solution {
public:
int longestPalindromeSubseq(string s) {
if (s.empty())
return 0;
int n = s.size();
vector<int> dp(n, 1);
for(int i = n-1; i>=0; --i) {
int prev = 0;
for(int j = i+1; j<n; ++j) {
int t = dp[j];
if(s[i] == s[j]) {
dp[j] = prev + 2;
}else {
dp[j] = max(dp[j - 1], dp[j]);
}
prev = t;
}
}
return dp[n - 1];
}
};
参考资料: