回文子序列问题解题模板及本质
回文子序列问题的本质:
- 回文子序列是一个动态规划相关的经典问题,其目标是寻找一个序列中的最长回文子序列。
- 回文子序列的本质特点:
- 回文的定义:从左到右读与从右到左读一样。
- 子序列的定义:不用连续,但保持相对位置。
- 核心思想:
- 通过状态转移和子问题划分解决问题。
- 回文的性质通常反映在递归和动态规划的左右扩展关系中。
经典问题及解题模板
1. Leetcode 516. 最长回文子序列
问题描述:
给定一个字符串 s
,寻找其中最长的回文子序列的长度。
解法特点:
- 使用动态规划解决问题,通过判断两端字符是否相等来决定状态转移。
- 经典的二维动态规划问题。
解题模板:动态规划
核心步骤:
- 定义状态:
- 定义
dp[i][j]
表示字符串s[i...j]
的最长回文子序列长度。 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])
]
两端字符无法同时参与回文,则选择舍弃一侧。
- 如果
- 初始化:
- 对于单字符回文:
dp[i][i] = 1
。 - 对于空区间:
dp[i][j] = 0
(j < i
)。
- 对于单字符回文:
- 结果:
- 答案是
dp[0][n-1]
,即从字符串的起点到终点的最长回文子序列长度。
- 答案是
代码模板:动态规划
java
class Solution {
public int longestPalindromeSubseq(String s) {
int n = s.length();
// 初始化 DP 数组
int[][] dp = new int[n][n];
// 初始化单字符的情况
for (int i = 0; i < n; i++) {
dp[i][i] = 1; // 每个单字符自身构成回文
}
// 从短区间向长区间递推
for (int len = 2; len <= n; len++) { // 子区间长度
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1; // 计算右边界
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]); // 两端不相等
}
}
}
return dp[0][n - 1]; // 全区间最长回文长度
}
}
复杂度分析
-
时间复杂度:
- 外层子区间循环需要进行 (O(n^2)) 计算。
- 每次计算时,只需根据公式访问相关状态。
- 总时间复杂度为 (O(n^2))。
-
空间复杂度:
- 二维 DP 数组占用 (O(n^2))。
本问题扩展:逻辑与技巧
2. Leetcode 1312. 让字符串成为回文的最少插入次数
问题描述:
给定一个字符串 s
,返回将其转化为回文字符串的最少插入次数。
本质和解法:
- 本质:求一个字符串的"最少插入次数",实际上可以转化为寻找"最长回文子序列"的补集长度。
- 公式关系:
最少插入次数 = 字符串长度 - 最长回文子序列的长度。
代码模板:动态规划
java
class Solution {
public int minInsertions(String s) {
int n = s.length();
// 这里复用最长回文子序列的 DP 定义
int[][] dp = new int[n][n];
// 初始化单字符情况
for (int i = 0; i < n; i++) {
dp[i][i] = 1;
}
// 从短区间向长区间递推
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
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]);
}
}
}
return n - dp[0][n - 1]; // 补集长度
}
}
3. Leetcode 647. 回文子串
问题描述:
给定一个字符串 s
,计算该字符串中有多少个回文子串。
代码模板:动态规划解决回文子串问题
- 核心思路:
通过动态规划记录每个子区间是否是回文。
java
class Solution {
public int countSubstrings(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n]; // 定义 DP 表:dp[i][j] 表示 s[i...j] 是否是回文
int count = 0; // 记录回文子串的数量
// 遍历子区间长度
for (int len = 1; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1; // 计算右边界
if (len == 1) { // 单字符回文
dp[i][j] = true;
} else if (len == 2) { // 两个字符,仅限相等情况
dp[i][j] = s.charAt(i) == s.charAt(j);
} else { // 多字符的情况
dp[i][j] = s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1];
}
if (dp[i][j]) {
count++; // 如果是回文子串,计数加一
}
}
}
return count; // 返回总数
}
}
如何快速写出 AC 代码
- 明确子问题的递归关系:右边界和子问题依赖;
- 初始化边界条件,例如单字符、空区间;
- 状态转移的方向是从短区间到长区间,逐步递推;
- 边界处理清晰,提升鲁棒性。
典型例题总结
- 最长回文子序列问题:
Leetcode 516
- 最少插入次数:
Leetcode 1312
- 回文子串数量:
Leetcode 647
- 问题转化扩展: 通过 DP 模板处理多种回文字符串问题,灵活应用递归关系。
通过动态规划模板可以快速实现并解决回文相关问题,适配多种场景,非常适合面试。