LeetCode 516:最长回文子序列
🔗 题目链接
👉 https://leetcode.cn/problems/longest-palindromic-subsequence/
📖 题目概要
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列 定义:不要求字符连续,在不改变原有字符顺序的前提下,可以删除某些字符(也可以不删除)得到新字符串。
回文定义:正读和反读完全一致的字符串。
示例
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb"。
示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb"。
约束条件
1 <= s.length <= 1000s仅由小写英文字母组成
📌 题目考点
- 经典区间动态规划
- 回文子序列 DP 模型(与最长回文子串区分)
- 区间 DP 遍历顺序、状态转移逻辑考察
- 字符串动态规划高频面试题
💡 解题思路
本题和「最长回文子串」不同:子序列不要求连续,因此解题思路采用标准区间 DP 求解。
1. 状态定义
定义二维 dp 数组:
dp[i][j] 表示字符串区间 s[i ... j] 中,最长回文子序列的长度。
2. 初始状态
单个字符一定是回文子序列,长度为 1。
因此初始化:dp[i][i] = 1(所有对角线位置赋值为 1)。
3. 状态转移方程
分两种情况讨论区间 [i, j]:
-
两端字符相等(
s[i] == s[j])两端字符可以同时加入回文序列,结果为内部区间最长回文长度 + 2
dpij=dpi+1j−1+2dpij = dpi+1j-1 + 2dpij=dpi+1j−1+2
-
两端字符不相等(
s[i] != s[j])只能舍弃左端字符 或 舍弃右端字符,取两种情况的最大值:
dpij=max(dpi+1j,dpij−1)dpij = \max(dpi+1j, dpij-1)dpij=max(dpi+1j,dpij−1)
4. 遍历顺序(核心要点)
dp[i][j] 的结果依赖于:dp[i+1][j-1]、dp[i+1][j]、dp[i][j-1],也就是当前区间依赖更小的内层区间 / 下一行 / 前一列 。
因此遍历规则:
- 左边界
i:从后往前遍历(从字符串末尾向开头) - 右边界
j:从 i+1 开始向后遍历(保证区间长度由小到大)
5. 最终答案
整个字符串对应区间 [0, len-1],最终返回 dp[0][len-1]。
✅ AC 代码
java
class Solution {
public int longestPalindromeSubseq(String s) {
char[] ss = s.toCharArray();
int len = s.length();
int [][] dp = new int[len+1][len+1];
// 初始化:单个字符最长回文子序列长度为 1
for(int i = 0; i < len; i++) {
dp[i][i] = 1;
}
// 左边界从后往前遍历
for(int i = len - 1; i >= 0; i--) {
// 右边界从 i+1 开始
for(int j = i + 1; j < len; j++) {
if(ss[i] == ss[j]) {
// 两端字符相等,内部结果 + 2
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][len-1];
}
}
⏱️ 复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n2)O(n^2)O(n2) | 两层循环遍历所有区间,nnn 为字符串长度 |
| 空间复杂度 | O(n2)O(n^2)O(n2) | 开辟二维 dp 数组存储区间状态 |
💡 补充总结
- 区分易混点:最长回文子串(连续)、最长回文子序列(不连续),二者均常用区间 DP,但状态转移逻辑不同。
- 遍历顺序:区间 DP 通用规律,依赖内层子区间时,左边界逆序遍历。
- 基础边界:单个字符回文长度固定为 1,是 DP 初始化的关键。