516. 最长回文子序列
问题描述
给定一个字符串 s,找到其中最长的回文子序列的长度。子序列不要求连续。
示例:
输入: "bbbab"
输出: 4
解释: 最长回文子序列是 "bbbb"
输入: "cbbd"
输出: 2
解释: 最长回文子序列是 "bb"
算法思路
动态规划(DP 数组):
- 状态定义:
dp[i][j]表示子串s[i..j]的最长回文子序列长度。
- 状态转移:
- 当
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])
- 当
- 初始化:
- 单个字符是长度为 1 的回文:
dp[i][i] = 1。 - 相邻字符相同则为 2:
dp[i][i+1] = 2 if s[i]==s[i+1]。
- 单个字符是长度为 1 的回文:
- 遍历顺序:
- 从下往上(
i从n-1到0),从左往右(j从i+1到n-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"
- 二维 DP 初始化:
- 对角线
dp[i][i] = 1
- 对角线
- 状态转移:
i=3, j=4:'a' != 'b'→dp[3][4]=max(dp[4][4], dp[3][3])=max(1,1)=1i=2, j=3:'b' != 'a'→dp[2][3]=max(dp[3][3], dp[2][2])=1i=2, j=4:'b' == 'b'→dp[2][4]=dp[3][3]+2=3i=1, j=2:'b' == 'b'→dp[1][2]=2i=1, j=3:'b' != 'a'→dp[1][3]=max(dp[2][3], dp[1][2])=max(1,2)=2i=1, j=4:'b' == 'b'→dp[1][4]=dp[2][3]+2=1+2=3i=0, j=1:'b' == 'b'→dp[0][1]=2i=0, j=2:'b' == 'b'→dp[0][2]=dp[1][1]+2=3i=0, j=3:'b' != 'a'→dp[0][3]=max(dp[1][3], dp[0][2])=max(2,3)=3i=0, j=4:'b' == 'b'→dp[0][4]=dp[1][3]+2=2+2=4
- 结果:
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
}
关键点
- 状态转移逻辑:
- 首尾相同:长度 = 内部子串 + 2。
- 首尾不同:长度 = 左侧或下方子串的最大值。
- 遍历顺序:
- 从下往上确保
dp[i+1][*]已计算。 - 从左往右确保
dp[i][j-1]已计算。
- 从下往上确保
- 空间优化核心:
pre保存dp[i+1][j-1]。temp保存dp[i+1][j]避免覆盖。
常见问题
- 为什么
dp[i][j]依赖dp[i+1][j-1]?
当首尾相同时,需加上内部子串的最长回文子序列长度。 - 如何处理相邻字符?
当j=i+1时,dp[i+1][j-1]越界视为 0,此时dp[i][j]=0+2=2。 - 一维 DP 中
pre的作用?
保存dp[i+1][j-1]的值,避免被覆盖。
2070

被折叠的 条评论
为什么被折叠?



