动态规划 最长回文子序列

516. 最长回文子序列

问题描述

给定一个字符串 s,找到其中最长的回文子序列的长度。子序列不要求连续。

示例

输入: "bbbab"
输出: 4
解释: 最长回文子序列是 "bbbb"

输入: "cbbd"
输出: 2
解释: 最长回文子序列是 "bb"

算法思路

动态规划(DP 数组)

  1. 状态定义
    • dp[i][j] 表示子串 s[i..j] 的最长回文子序列长度。
  2. 状态转移
    • s[i] == s[j] 时:
      dp[i][j] = dp[i+1][j-1] + 2
    • s[i] != s[j] 时:
      dp[i][j] = max(dp[i+1][j], dp[i][j-1])
  3. 初始化
    • 单个字符是长度为 1 的回文:dp[i][i] = 1
    • 相邻字符相同则为 2:dp[i][i+1] = 2 if s[i]==s[i+1]
  4. 遍历顺序
    • 从下往上(in-10),从左往右(ji+1n-1)。

空间优化(一维 DP)

  • 由于 dp[i][j] 仅依赖左侧(dp[i][j-1])、下方(dp[i+1][j])和左下角(dp[i+1][j-1]),可用一维数组代替二维数组。
  • pre 保存左下角值,避免覆盖。

代码实现

方法一:动态规划(DP 数组)

class Solution {
    public int longestPalindromeSubseq(String s) {
        if (s == null || s.isEmpty()) return 0;
        
        int n = s.length();
        int[][] dp = new int[n][n]; // dp[i][j] 表示 s[i..j] 的最长回文子序列长度
        
        // 初始化:单个字符的回文长度为 1
        for (int i = 0; i < n; i++) {
            dp[i][i] = 1;
        }
        
        // 从下往上遍历
        for (int i = n - 1; i >= 0; i--) {
            // 从左往右遍历(j 从 i+1 开始)
            for (int j = i + 1; j < n; j++) {
                // 首尾字符相同
                if (s.charAt(i) == s.charAt(j)) {
                    // 当 j=i+1 时,dp[i+1][j-1] 未定义,视为 0
                    int inner = (j - i > 1) ? dp[i + 1][j - 1] : 0;
                    dp[i][j] = inner + 2;
                } else {
                    // 取左侧或下方的最大值
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[0][n - 1];
    }
}

方法二:动态规划(空间优化)

class Solution {
    public int longestPalindromeSubseq(String s) {
        if (s == null || s.isEmpty()) return 0;
        
        int n = s.length();
        int[] dp = new int[n]; // dp[j] 表示当前行以 j 结尾的子串的最长回文子序列长度
        Arrays.fill(dp, 1);    // 初始化:每个字符自身长度为 1
        
        for (int i = n - 1; i >= 0; i--) {
            int pre = 0; // 保存 dp[i+1][j-1] 的值
            for (int j = i + 1; j < n; j++) {
                int temp = dp[j]; // 保存旧值(即 dp[i+1][j])
                if (s.charAt(i) == s.charAt(j)) {
                    // 状态转移:dp[i][j] = pre + 2
                    dp[j] = pre + 2;
                } else {
                    // 状态转移:dp[i][j] = max(dp[i+1][j], dp[i][j-1])
                    dp[j] = Math.max(dp[j], dp[j - 1]);
                }
                pre = temp; // 更新 pre 为 dp[i+1][j](用于下一轮作为 dp[i+1][j-1])
            }
        }
        return dp[n - 1];
    }
}

算法分析

  • 时间复杂度:O(n²)
    两层循环遍历所有子串。
  • 空间复杂度
    • 二维 DP:O(n²)
    • 一维 DP:O(n)

算法过程

输入s = "bbbab"

  1. 二维 DP 初始化
    • 对角线 dp[i][i] = 1
  2. 状态转移
    • i=3, j=4'a' != 'b'dp[3][4]=max(dp[4][4], dp[3][3])=max(1,1)=1
    • i=2, j=3'b' != 'a'dp[2][3]=max(dp[3][3], dp[2][2])=1
    • i=2, j=4'b' == 'b'dp[2][4]=dp[3][3]+2=3
    • i=1, j=2'b' == 'b'dp[1][2]=2
    • i=1, j=3'b' != 'a'dp[1][3]=max(dp[2][3], dp[1][2])=max(1,2)=2
    • i=1, j=4'b' == 'b'dp[1][4]=dp[2][3]+2=1+2=3
    • i=0, j=1'b' == 'b'dp[0][1]=2
    • i=0, j=2'b' == 'b'dp[0][2]=dp[1][1]+2=3
    • i=0, j=3'b' != 'a'dp[0][3]=max(dp[1][3], dp[0][2])=max(2,3)=3
    • i=0, j=4'b' == 'b'dp[0][4]=dp[1][3]+2=2+2=4
  3. 结果dp[0][4]=4

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1: 标准示例
    String s1 = "bbbab";
    System.out.println("Test 1: " + solution.longestPalindromeSubseq(s1)); // 4
    
    // 测试用例2: 连续相同字符
    String s2 = "cbbd";
    System.out.println("Test 2: " + solution.longestPalindromeSubseq(s2)); // 2
    
    // 测试用例3: 全相同字符
    String s3 = "aaaa";
    System.out.println("Test 3: " + solution.longestPalindromeSubseq(s3)); // 4
    
    // 测试用例4: 单字符
    String s4 = "a";
    System.out.println("Test 4: " + solution.longestPalindromeSubseq(s4)); // 1
    
    // 测试用例5: 无回文子序列(最长1)
    String s5 = "abc";
    System.out.println("Test 5: " + solution.longestPalindromeSubseq(s5)); // 1
    
    // 测试用例6: 长字符串
    String s6 = "abcdefghijklmnopqrstuvwxyz";
    System.out.println("Test 6: " + solution.longestPalindromeSubseq(s6)); // 1
}

关键点

  1. 状态转移逻辑
    • 首尾相同:长度 = 内部子串 + 2。
    • 首尾不同:长度 = 左侧或下方子串的最大值。
  2. 遍历顺序
    • 从下往上确保 dp[i+1][*] 已计算。
    • 从左往右确保 dp[i][j-1] 已计算。
  3. 空间优化核心
    • pre 保存 dp[i+1][j-1]
    • temp 保存 dp[i+1][j] 避免覆盖。

常见问题

  1. 为什么 dp[i][j] 依赖 dp[i+1][j-1]
    当首尾相同时,需加上内部子串的最长回文子序列长度。
  2. 如何处理相邻字符?
    j=i+1 时,dp[i+1][j-1] 越界视为 0,此时 dp[i][j]=0+2=2
  3. 一维 DP 中 pre 的作用?
    保存 dp[i+1][j-1] 的值,避免被覆盖。
最长回文子序列(Longest Palindromic Subsequence,LPS)问题是指在一个给定的字符串中找到一个最长回文子序列回文子序列是指一个序列本身不是回文串,但它是一个回文串的子序列。 在C++中,我们可以使用动态规划(Dynamic Programming,DP)的方法来解决这个问题。动态规划的主要思想是将一个大问题分解成小问题,然后从小问题出发,逐渐求得大问题的解。 以下是一个使用动态规划解决最长回文子序列问题的C++示例代码: ```cpp #include <iostream> #include <vector> #include <string> using namespace std; // 函数用于计算字符串str的最长回文子序列的长度 int longestPalindromeSubseq(string str) { int n = str.size(); // 创建一个二维数组dp,用于存储子问题的解,初始化所有值为0 vector<vector<int>> dp(n, vector<int>(n, 0)); // 单个字符的最长回文子序列长度为1,所以对角线上的元素设置为1 for (int i = 0; i < n; i++) { dp[i][i] = 1; } // 如果两个字符相同,那么它俩组成的子序列长度为2 for (int cl = 2; cl <= n; cl++) { for (int i = 0; i < n - cl + 1; i++) { int j = i + cl - 1; if (str[i] == str[j] && cl == 2) { dp[i][j] = 2; } else if (str[i] == str[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]; } int main() { string str; cout << "请输入一个字符串:" << endl; cin >> str; cout << "最长回文子序列的长度为:" << longestPalindromeSubseq(str) << endl; return 0; } ``` 在这段代码中,`dp[i][j]`表示从字符串的第`i`个字符到第`j`个字符组成的子串的最长回文子序列的长度。通过初始化对角线以及递推式逐步填充这个二维数组,最终可以得到整个字符串的最长回文子序列长度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值