本文正在参加「Java主题月 - Java 刷题打卡」,详情查看活动链接
一、题目概述
子序列问题是最常见的算法问题,而且并不好解决。
一旦涉及子序列和最值,那几乎可以肯定,考察的是动态规划技巧,时间复杂度一般都是 O(n ^ 2)
既然用到动态规划,那就要定义
dp数组,寻找状态转移关系。
两种思路:
- 第一种思路模板是一个一维的
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] + ...)
}
}
- 第二种思路模板是一个二维的
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]的字符。
如图:
- 如果
s[i] == s[j]:那么它俩加上s[i+1...j-1]中的最长回文子序列就是s[i...j]的最长回文子序列
如图:
- 如果
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]。
二、思路实现
思路:
- 明确基本情况
base case:如果一个字符,最长回文子序列长度是 1,dp[i][j] = 1 (i == j) - 因为
i肯定小于或等于j,所以对于那些i > j的位置,根本不存在子序列,初始化为 0
根据状态转移方程,想求 dp[i][j] 需要知道 dp[i + 1][j - 1]、dp[i + 1][j]、dp[i][j - 1] 这三个位置,如图:
为了保证每次计算 dp[i][j],左下右方向的位置已经被计算出来,只能斜着遍历或者反着遍历,如图:
这里选择反向遍历。
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];
}
}