今天复盘两道面试高频的字符串动态规划题:「最长回文子串」和「最长公共子序列」。一道是字符串回文问题的基础,另一道是双字符串 DP 的经典模板,两者都是字符串处理的核心考点。
一、5. 最长回文子串(中等)
题目描述
给你一个字符串 s,找到 s 中最长的回文子串。
核心思路 1:中心扩展法(直观高效)
回文的本质是对称,所以我们可以把每个字符(奇数长度)和每对相邻字符(偶数长度)当作中心,向两边扩展,找到以该中心为对称轴的最长回文子串。
- 对于每个中心
i:- 奇数长度:以
(i, i)为中心扩展 - 偶数长度:以
(i, i+1)为中心扩展
- 奇数长度:以
- 每次扩展时,只要左右字符相等就继续,直到不相等为止,记录当前最长的子串。
完整代码(Java)
java
运行
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
private int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return right - left - 1;
}
}
核心思路 2:动态规划
定义 dp[i][j] 表示字符串 s[i..j] 是否为回文子串:
- 状态转移:
dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1] - 边界条件:
- 单个字符:
dp[i][i] = true - 两个字符:
dp[i][i+1] = (s[i] == s[i+1])
- 单个字符:
- 遍历方式:按子串长度从小到大遍历,避免依赖未计算的状态。
时间与空间复杂度
- 中心扩展法:时间 O (n²),空间 O (1)
- 动态规划:时间 O (n²),空间 O (n²)(可优化为一维)
二、1143. 最长公共子序列(中等)
题目描述
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 0。
核心思路:二维动态规划
这是双字符串 DP 的经典模板题,核心是定义状态并利用 "选或不选" 的思想转移:
- 定义
dp[i][j]表示text1[0..i-1]和text2[0..j-1]的最长公共子序列长度。 - 状态转移:
- 如果
text1[i-1] == text2[j-1]:dp[i][j] = dp[i-1][j-1] + 1(当前字符匹配,长度 + 1) - 如果不相等:
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])(选其中一个字符串的前一个状态)
- 如果
- 边界条件:
dp[0][j] = 0,dp[i][0] = 0(空字符串的公共子序列长度为 0)
完整代码(Java)
java
运行
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
}
优化:一维 DP
因为每次更新 dp[i][j] 只需要上一行的 dp[i-1][j] 和当前行的 dp[i][j-1],可以将二维数组压缩为一维,空间复杂度从 O (mn) 降到 O (min (m,n)):
java
运行
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
if (text1.length() < text2.length()) {
return longestCommonSubsequence(text2, text1);
}
int m = text1.length();
int n = text2.length();
int[] dp = new int[n + 1];
for (int i = 1; i <= m; i++) {
int prev = 0;
for (int j = 1; j <= n; j++) {
int temp = dp[j];
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[j] = prev + 1;
} else {
dp[j] = Math.max(dp[j], dp[j - 1]);
}
prev = temp;
}
}
return dp[n];
}
}
两道题对比总结
表格
| 题目 | 类型 | 核心思想 | 时间复杂度 | 空间复杂度 | 关键考点 |
|---|---|---|---|---|---|
| 最长回文子串 | 字符串回文 / DP | 中心扩展 / DP 状态转移 | O(n²) | O (1)(中心扩展) | 回文对称性、子串与子序列的区别 |
| 最长公共子序列 | 双字符串 DP | 选或不选的状态转移 | O(mn) | O (min (m,n))(优 |