【DP】子序列解题模板 - 最长回文子序列|Java 刷题打卡

784 阅读2分钟

本文正在参加「Java主题月 - Java 刷题打卡」,详情查看活动链接

一、题目概述

子序列问题是最常见的算法问题,而且并不好解决。

一旦涉及子序列和最值,那几乎可以肯定,考察的是动态规划技巧,时间复杂度一般都是 O(n ^ 2)

既然用到动态规划,那就要定义 dp 数组,寻找状态转移关系。

两种思路:

  1. 第一种思路模板是一个一维的 dp 数组:
int n = array.length;
int [] dp = new int[n];

for (int i = 1; i < n; ++i) {
    for (int j = 0; j < i; ++j) {
        dp[i] = 最值(dp[i], dp[j] + ...)
    }
}
  1. 第二种思路模板是一个二维的 dp 数组:
int n = arr.length;
int [][] dp = new int[n][n];

for (int i = 0; i < n; ++i) {
    for (int j = 0; j < n; ++j) {
        if (arr[i] == arr[j]) 
            dp[i][j] = dp[i][j] + ...
        else
            dp[i][j] = 最值(...)
    }
}

LeetCode 516. 最长回文子序列

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。

示例 1:

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



示例 2:

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

题干分析

比如输入 s= "aecda", 算法返回 3, 因为最长回文子序列是 "aca", 长度为 3

这个问题对 dp数组的定义是:在子串 s[i..j] 中,最长回文子序列的长度为 dp[i][j]

为什么这个问题要这样定义二维的 dp 数组呢?

找状态转移需要归纳思维,说白了就是如何从已知的结果推出未知的部分,这样定义容易归纳,容易发现状态转移关系。

假设知道了子问题 dp[i + 1][j - 1] 结果(s[i + 1 ... j - 1]中最长回文子序列的长度),是否能算出 dp[i][j]的值?

可以,这取决于 s[i]s[j] 的字符。

如图:

  1. 如果 s[i] == s[j]:那么它俩加上 s[i+1...j-1] 中的最长回文子序列就是 s[i...j] 的最长回文子序列

如图:

dp-最长回文子序列1.png

dp-最长回文子序列2.png

  1. 如果 s[i] != s[j]:说明它俩不可能同时出现在 s[i...j] 的最长回文子序列中,那么把它俩分别加入 s[i+1...j-1] 中,看看哪个子串产生的回文子序列更长。

如图:

逻辑如下:

if (s[i] == s[j])
    // 它俩一定在最长回文子序列中
    dp[i][j] = dp[i + 1][j - 1] + 2;
else 
    // s[i+1 .. j] 和 s[i..j - 1] 谁的回文子序列更长?
    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);

整个 s 的最长回文子序列的长度:dp[0][n - 1]



二、思路实现

思路:

  1. 明确基本情况 base case:如果一个字符,最长回文子序列长度是 1, dp[i][j] = 1 (i == j)
  2. 因为 i 肯定小于或等于 j,所以对于那些 i > j 的位置,根本不存在子序列,初始化为 0

根据状态转移方程,想求 dp[i][j] 需要知道 dp[i + 1][j - 1]dp[i + 1][j]dp[i][j - 1] 这三个位置,如图:

为了保证每次计算 dp[i][j],左下右方向的位置已经被计算出来,只能斜着遍历或者反着遍历,如图:

dp-最长回文子序列5.png

dp-最长回文子序列3.png

这里选择反向遍历。

public class LeetCode_516 {
    // Time: O(n ^ 2), Space: O(n ^ 2), Faster: 74.25%
    public int longestPalindromeSubseq(String s) {

        if (s == null || s.length() == 0) return 0;
        int n = s.length();
        int [][] dp = new int[n][n];

        for (int i = 0; i < n; ++i)
            dp[i][i] = 1;
        // 反向遍历保证正确的状态转移
        for (int i = n - 2; i >= 0; --i) {
            for (int j = i + 1; j < n; ++j) {
                // 状态转移方程
                if (s.charAt(i) == s.charAt(j))
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                else
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
            }
        }
        // 整个 s 的最长回文子序列长度
        return dp[0][n - 1];
    }
}