最长回文子序列

问题描述:给定一个字符串,找到其中最长的回文子序列,并返回该序列的长度。字符串长度<=5000
示例
输入:"abccsb"
返回值:4
说明:子序列bccb回文序列为bccb,因此最长的回文子序列长度为4

方法一

思路分析

     最长回文子序列表示子序列在给定的序列中位置可以是不连续的。使用递归的办法,采用自上而下的办法,要求的string[0,n-1]中开始元素到末尾元素中间的最长回文子序列,有如下递归方程:
  • 设置$lps(string ,i,j)$表示字符串string中第i个位置到第j个位置中的最长回文子序列
  • 设置$lps(string ,i,j)=1,i=j$,同时这也是递归结束的条件
  • 如果首尾元素相同,那么$lps(string ,i,j)=lps(string ,i+1,j-1)+2$
  • 如果首尾元素不同,那么$lps(string ,i,j)=max\{ lps(string ,i+1,j),lps(string ,i,j-1)\}$

图解

输入:"abccsb",递归过程如下图:
0 1 2 3 4 5
a b c c s b



C++核心代码

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string 一个字符串由小写字母构成,长度小于5000
     * @return int
     */
    int longestPalindromeSubSeq(string s) {
        // write code here
        int n=s.size();
        return lps(s,0,n-1);
    }
    int lps(string str, int i, int j)
   {
	if (i == j)    
		return 1;	//只有一个元素,回文长度为1
	if (i > j) return 0;   //因为只计算序列str[i....j]
 
	//如果首尾相同
	if (str[i] == str[j])
		return lps(str, i + 1, j - 1) + 2;
	//如果首尾不同
	return max(lps(str, i, j - 1), lps(str, i + 1, j));
    }

};

复杂度分析

  • 时间复杂度:有许多相同的子问题会被重复计算,这是递归的一个问题,递归的时间复杂度为递归的次数*每次递归的操作次数,递归次数为$n^2$,因此时间复杂度为$O(n^2)$,该方***超时
  • 空间复杂度:递归的空间复杂度为递归的深度*每次递归创建的变量,递归深度为$n$,每次递归的空间复杂度为$O(1)$,因此空间复杂度为$O(n)$

方法二

思路分析

     在使用递归的方法时由于采用自上而下的办法,因此有些子问题被重复计算,为了避免这种问题,我们还可以采用自下而上的办法,即采用动态规划的办法,即借助一个二维数组dp[i][j]存储已经计算过的子问题,即第i个位置到第j个位置最长回文子序列的长度。使用动态规划的转移方程为:
  • 所有连续的长度为i+1的子串,$str[j..j+i]$
  • 如果$dp[i][i]=1 i=j$
  • 如果首尾元素相同那么$dp[j][j+i]=dp[j+1][j+i-1]+2$
  • 如果首尾元素不同那么$dp[j][j+i]=max\{dp[j+1][j+i],dp[j][j+i-1]\}$
  • 最后输出$dp[0][n-1]$即为给定字符串的最长回文子串

C++核心代码

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string 一个字符串由小写字母构成,长度小于5000
     * @return int
     */
    int longestPalindromeSubSeq(string s) {
        // write code here
        int n=s.size();
        return lpsDp(s,n);
    }
    int lpsDp(string str, int n)
    {
	int  tmp;
    vector<vector<int>> dp(n,vector<int>(n,0));
	for (int i = 0; i < n; ++i) 	
        dp[i][i] = 1;
	for (int i = 1; i < n; ++i)
	{
		tmp = 0;
		//考虑所有连续的长度为i+1的子串,str[j....j+i]
		for (int j = 0; j + i < n; j++)
		{
			if (str[j] == str[j + i])//如果首尾相同
				tmp = dp[j + 1][j + i - 1] + 2;
			else //如果首尾不同
				tmp = max(dp[j + 1][j + i], dp[j][j + i - 1]);
			dp[j][j + i] = tmp;
		}
	}
	return dp[0][n - 1]; //返回字符串str[0...n-1]的最长回文子序列长度
}
};

复杂度分析

  • 时间复杂度:内外层循环次数为$1+2+3+...+n=n*(n+1)/2$,因此时间复杂度为$O(n^2)$
  • 空间复杂度:借助于一个二维数组存储当前位置的最长回文子序列长度,空间复杂度为$O(n^2)$


方法三

思路分析

      为进一步减小空间的使用,在方法二中可以发现求解$dp[i][j]$数组只与相邻状态有关,因此可以设置一个一维的数组。

python核心代码

#
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
#
# 
# @param s string 一个字符串由小写字母构成,长度小于5000
# @return int
#
class Solution:
    def longestPalindromeSubSeq(self , s ):
        # write code here
        dp = [1] * len(s)
        for i in range(len(s)-1, -1, -1):
            pre = 0      # dp[i+1][j-1]
            for j in range(i+1, len(s)):
                temp = dp[j]
                if s[i] == s[j]:
                    dp[j] = pre + 2
                else:
                    dp[j] = max(dp[j], dp[j-1])
                pre = temp
        return dp[-1]

复杂度分析

  • 时间复杂度:内外层循环次数为$1+2+3...+n=n*(n+1)/2$,因此时间复杂度为$O(n^2)$
  • 空间复杂度:借助于一个一维数组,空间复杂度为$O(1)$