DP系列-最长回文子序列

原题意:
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
回文序列:如果一个序列逆置之后跟原序列是一样的就称这样的序列为回文序列,如aba、123321等。
示例1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

常规推导
定义字符串s长度为m,定义函数f(i, j)为字符串s在[i,j]区间的最长回文子序列,有三种选择:

即:
1、当s的长度为1时,则s本身就是回文串,并且最长长度为1。
2、当s的i位置上的字符等于s的j位置上的字符时,长度加1,然后再递归去比较s的i+1和s的j-1位置字符。
3、当s[i]不等于s[j]时,分两种情况,第一种s[i+1]和s[j]位置比较,第二种s[i]和s[j-1]位置比较,因为要取最长长度,所以取两种情况的最大值。
定义字符串s="bbbab",则:
当i=0,j=4时s[0] == s[4],然后去递归i+1和j-1的位置,并且此时的长度为2。

当i=1,j=3时s[1] != s[3],此时需要比较i、j-1位置和i+1、j位置,即:(s[1]、s[2]和s[2]、s[3])然后取两者最大值。

可以看出i=1,j=2时,s[1]==s[2],当i=2,j=3时,s[2]!=s[3],所以继续递归i=1,j=2并且长度加2。

当i=2,j=1时,已经超出区间[i,j]。

根据推导过程,尝试着写出代码:

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        return self.process(s, 0, len(s) - 1)

    def process(self, s, i, j):
        """
        s在区间[i,j]上的最长回文子序列
        """
        # 超出区间直接退出
        if i < 0 or j > len(s) or i > j:
            return 0
        # 只有一个字符时,长度为1
        if i == j:
            return 1
            
        if s[i] == s[j]:
            return self.process(s, i + 1, j - 1) + 2
        return max(self.process(s, i + 1, j), self.process(s, i, j - 1))

分析下,时间复杂度,空间复杂度为O(m)。这样数据量大的时候,非常耗时的,因为有很多重复的运算,解决办法就是加缓存,让重复的计算只进行一次。
记忆化搜索
分析下process函数中只有i和j的变化会得到不同的结果,所以定义二维数组cache,初始化为None,长宽分别为s的长度m,因为字符串索引从0开始,所以字符串的长度可以包含所有字符串。

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        m = len(s)
        cache = [[None] * (m + 1) for _ in range(m + 1)]
        return self.process(s, 0, len(s) - 1, cache)

    def process(self, s, m, n, cache):
        """
        带缓存的,符串在m~n上的最长回文子序列
        """
        if m < 0 or n > len(s) or m > n:
            return 0
        if m == n:
            return 1

        if cache[m][n]:
            return cache[m][n]
        if s[m] == s[n]:
            result = self.process1(s, m + 1, n - 1, cache) + 2
        else:
            result = max(self.process1(s, m + 1, n, cache), self.process1(s, m, n - 1, cache))
        return result

动态规划
根据递归试着画出动态规划的转移图,我们需要设置一个二维数组dp,dp[i][j]表示字符串s在区间[i,j]上的最长回文子序列的长度,所以我们求得dp[0][m]就代表最终的答案,并且dp[i][j]依赖i+1、j-1的位置,每个格子都依赖下一行和上一列,所以填表的时候,从下往上,从左往右开始。
转换图如下:

当i==j(对角线的位置)时表示单个字符,并且单个字符的最长回文子串长度就是1,
状态转移为

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])

根据填表过程写出代码:

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        """
        动态规划
        :param s:
        :return:
        """
        n = len(s)
        dp = [[0] * n for _ in range(n)]
        # 因为每格都依赖下一行的值和上一列的值,所以从下往上,从左往右填值
        for i in range(n - 1, -1, -1):
            # 每个字符串自己就是长度为1的回文序列
            dp[i][i] = 1
            # 只需要对角线右边的值,所以从i+1开始
            for j in range(i + 1, n):
                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]

其中时间复杂度为,空间复杂度为

posted @ 2023-01-02 16:20  ImLiFeLong  阅读(148)  评论(0编辑  收藏  举报