Medium

题目描述

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列是一个可以从另一个序列中删除某些或不删除元素而不改变其余元素顺序的序列。

示例 1:

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

示例 2:

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

提示:

  • 1 <= s.length <= 1000
  • s 只包含小写英文字母

解题思路

这是一道经典的动态规划题目,核心思想是通过区间DP来解决。

解题思路:

定义 dp[i][j] 表示字符串 s[i...j] 中最长回文子序列的长度。状态转移分为以下几种情况:

  1. i == j 时,单个字符本身就是回文,所以 dp[i][j] = 1
  2. s[i] == s[j] 时,两端字符相同,可以同时选择,因此 dp[i][j] = dp[i+1][j-1] + 2
  3. s[i] != s[j] 时,两端字符不同,只能选择其中一个,取最大值:dp[i][j] = max(dp[i+1][j], dp[i][j-1])

解法分析:

  1. 区间DP(推荐):从短区间到长区间逐步计算,时间复杂度O(n²),空间复杂度O(n²)
  2. 记忆化递归:自顶向下的递归解法,本质与区间DP相同,但实现更直观

由于区间长度从小到大计算,外层循环遍历区间长度,内层循环遍历起始位置,确保计算顺序正确。

代码实现

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.length();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        
        // 单个字符的情况
        for (int i = 0; i < n; i++) {
            dp[i][i] = 1;
        }
        
        // 区间长度从2开始
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 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]);
                }
            }
        }
        
        return dp[0][n - 1];
    }
};
class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n = len(s)
        dp = [[0] * n for _ in range(n)]
        
        # 单个字符的情况
        for i in range(n):
            dp[i][i] = 1
        
        # 区间长度从2开始
        for length in range(2, n + 1):
            for i in range(n - length + 1):
                j = i + length - 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])
        
        return dp[0][n - 1]
public class Solution {
    public int LongestPalindromeSubseq(string s) {
        int n = s.Length;
        int[,] dp = new int[n, n];
        
        // 单个字符的情况
        for (int i = 0; i < n; i++) {
            dp[i, i] = 1;
        }
        
        // 区间长度从2开始
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 1;
                if (s[i] == s[j]) {
                    dp[i, j] = dp[i + 1, j - 1] + 2;
                } else {
                    dp[i, j] = Math.Max(dp[i + 1, j], dp[i, j - 1]);
                }
            }
        }
        
        return dp[0, n - 1];
    }
}
/**
 * @param {string} s
 * @return {number}
 */
var longestPalindromeSubseq = function(s) {
    const n = s.length;
    const dp = Array(n).fill().map(() => Array(n).fill(0));
    
    for (let i = 0; i < n; i++) {
        dp[i][i] = 1;
    }
    
    for (let len = 2; len <= n; len++) {
        for (let i = 0; i <= n - len; i++) {
            const j = i + len - 1;
            if (s[i] === s[j]) {
                dp[i][j] = dp[i + 1][j - 1] + 2;
            } else {
                dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
            }
        }
    }
    
    return dp[0][n - 1];
};

复杂度分析

方法时间复杂度空间复杂度
区间DPO(n²)O(n²)
记忆化递归O(n²)O(n²)

其中 n 为字符串的长度。区间DP需要填充n×n的二维表格,每个状态计算时间为O(1)。

相关题目