本次题目为以下四题
最小路径和
最长回文子串
最长公共子序列
编辑距离
64. 最小路径和

class Solution {
public int minPathSum(int[][] grid) {
// 边界校验
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
int m = grid.length; // 行数
int n = grid[0].length; // 列数
int[][] dp = new int[m][n];
// 初始化起点
dp[0][0] = grid[0][0];
// 初始化第一列
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i-1][0] + grid[i][0];
}
// 初始化第一行
for (int j = 1; j < n; j++) {
dp[0][j] = dp[0][j-1] + grid[0][j];
}
// 填充dp表(状态转移)
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
return dp[m-1][n-1];
}
}
解题思路1:动态规划
状态定义
定义 dp[i][j] 为从网格左上角 (0,0) 走到位置 (i,j) 的最小路径和。
边界条件
-
第一行 :只能从左侧向右移动,因此
dp[0][j] = dp[0][j-1] + grid[0][j]。 -
第一列 :只能从上方向下移动,因此
dp[i][0] = dp[i-1][0] + grid[i][0]。 -
起点 :
dp[0][0] = grid[0][0](初始位置的路径和为自身值)。
状态转移方程
对于非边界位置 (i,j),只能从上方 (i-1,j) 或左侧 (i,j-1) 到达,取两者中路径和较小的一方,加上当前格子的值:dp[i][j]=min(dp[i−1][j],dp[i][j−1])+grid[i][j]
最终结果
右下角位置 dp[m-1][n-1] 即为从起点到终点的最小路径和(m 为行数,n 为列数)
面试中常要求优化空间,由于原网格的数值在计算后无需保留,可直接在原网格上更新,省去额外的 dp 数组:
class Solution {
public int minPathSum(int[][] grid) {
if (grid == null || grid.length == 0) return 0;
int m = grid.length;
int n = grid[0].length;
// 初始化第一列
for (int i = 1; i < m; i++) {
grid[i][0] += grid[i-1][0];
}
// 初始化第一行
for (int j = 1; j < n; j++) {
grid[0][j] += grid[0][j-1];
}
// 原地状态转移
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
grid[i][j] += Math.min(grid[i-1][j], grid[i][j-1]);
}
}
return grid[m-1][n-1];
}
}
5. 最长回文子串

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);
}
// 中心扩展函数,返回以 left 和 right 为中心的最长回文长度
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;
}
}
解题思路1:中心扩展
枚举所有可能的中心:
-
奇数长度:以单个字符
s[i]为中心(共n个中心); -
偶数长度:以两个相邻字符
s[i]和s[i+1]为中心(共n-1个中心)。
中心扩展:对每个中心,向左右两侧扩散,直到字符不相等为止,记录当前回文长度。
记录最大值:遍历所有中心后,保留长度最长的回文子串的起止位置。
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";
int n = s.length();
// dp[i][j] 表示 s[i..j] 是否为回文
boolean[][] dp = new boolean[n][n];
int start = 0, maxLen = 1; // 初始最长回文长度为1(单个字符)
// 单个字符都是回文
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 遍历子串长度(从2到n)
for (int len = 2; len <= n; len++) {
// 遍历起始索引i
for (int i = 0; i < n - len + 1; i++) {
int j = i + len - 1; // 结束索引j
if (s.charAt(i) != s.charAt(j)) {
dp[i][j] = false;
} else {
// 长度为2时,直接为true;否则看内部子串
if (len == 2) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 更新最长回文子串
if (dp[i][j] && len > maxLen) {
maxLen = len;
start = i;
}
}
}
return s.substring(start, start + maxLen);
}
}
解题思路2:动态规划
动态规划的核心是利用已计算的子问题结果推导更大问题的解:
-
定义
dp[i][j]:表示字符串s中从索引i到j的子串是否为回文 -
状态转移:
-
若
i == j(单个字符):dp[i][j] = true -
若
j - i == 1(两个字符):dp[i][j] = (s[i] == s[j]) -
若
j - i > 1:dp[i][j] = (s[i] == s[j] && dp[i+1][j-1])
-
-
遍历顺序:按子串长度从小到大遍历(先算短子串,再算长子串
代码解释
-
dp二维数组:存储子串是否为回文的状态,避免重复计算 -
先初始化单个字符的回文状态(所有
dp[i][i] = true) -
按子串长度从 2 开始遍历,确保计算
dp[i][j]时,dp[i+1][j-1]已计算完成 -
每次发现更长的回文子串,更新起始索引和最大长度
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";// 步骤1:预处理字符串,插入#统一奇偶长度 StringBuilder sb = new StringBuilder(); sb.append('#'); for (char c : s.toCharArray()) { sb.append(c).append('#'); } String t = sb.toString(); int n = t.length(); // 步骤2:初始化Manacher算法变量 int[] p = new int[n]; // 存储每个位置的回文半径 int center = 0, right = 0; // 当前最长回文的中心和右边界 int maxLen = 0, maxCenter = 0; // 最长回文的半径和中心 // 步骤3:遍历处理每个位置 for (int i = 0; i < n; i++) { // 利用对称性快速计算p[i]的初始值 if (i < right) { int mirror = 2 * center - i; // i关于center的对称点 p[i] = Math.min(right - i, p[mirror]); } // 尝试扩展回文半径 int a = i + (1 + p[i]); int b = i - (1 + p[i]); while (a < n && b >= 0 && t.charAt(a) == t.charAt(b)) { p[i]++; a++; b--; } // 更新最长回文的中心和右边界 if (i + p[i] > right) { center = i; right = i + p[i]; } // 更新全局最长回文 if (p[i] > maxLen) { maxLen = p[i]; maxCenter = i; } } // 步骤4:还原原字符串的最长回文子串 int start = (maxCenter - maxLen) / 2; // 转换回原字符串的起始索引 return s.substring(start, start + maxLen); }}
解题思路3:Manacher 算法
Manacher 算法通过对称性 和边界扩展将时间复杂度优化到 O (n):
-
预处理字符串:在每个字符间插入特殊符号(如
#),统一奇偶长度回文的处理(例:babad→#b#a#b#a#d#) -
定义关键变量:
-
p[i]:以i为中心的最长回文半径(包含自身) -
center:当前最长回文的中心 -
right:当前最长回文的右边界
-
-
利用对称性快速计算
p[i],仅在必要时扩展边界
代码解释
-
预处理:插入
#后,所有回文都是奇数长度(如bb→#b#b#,中心在第二个#) -
对称性优化:
i < right时,p[i]先取对称点的半径或right - i(避免越界) -
扩展:仅在必要时向外扩展,减少重复比较
-
还原:通过
maxCenter和maxLen计算原字符串的起始索引,截取结果
1143. 最长公共子序列

class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
// dp[i][j] 表示 text1[0..i-1] 和 text2[0..j-1] 的LCS长度
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)) {
// 字符匹配,继承前一个子问题结果+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];
}
}
解题思路1:动态规划
状态定义 :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 -
若
text1[i-1] != text2[j-1]:取两种情况的最大值,dp[i][j] = max(dp[i-1][j], dp[i][j-1])
初始化 :dp[0][j] = 0、dp[i][0] = 0(空串与任何串的 LCS 长度为 0)
结果 :dp[text1.length][text2.length]
优化思路(一维 DP 版):
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
// 让较短的串作为列,优化空间
if (text2.length() > text1.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; // 保存 dp[i-1][j-1] 的值
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];
}
}
72. 编辑距离

class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// dp[i][j] 表示 word1[0..i-1] 转 word2[0..j-1] 的最小操作数
int[][] dp = new int[m + 1][n + 1];
// 初始化边界
for (int i = 0; i <= m; i++) {
dp[i][0] = i; // 全删除
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j; // 全插入
}
// 填充 dp 表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
// 取替换、删除、插入三种操作的最小值 +1
dp[i][j] = Math.min(
Math.min(dp[i - 1][j - 1], dp[i - 1][j]),
dp[i][j - 1]
) + 1;
}
}
}
return dp[m][n];
}
}
解题思路1:动态规划
状态定义 :dp[i][j] 表示将 word1[0..i-1] 转换为 word2[0..j-1] 所需的最少操作数。
状态转移:
-
若
word1[i-1] == word2[j-1]:字符相同,无需操作,dp[i][j] = dp[i-1][j-1] -
若
word1[i-1] != word2[j-1]:取以下三种操作的最小值 +1:-
替换:
dp[i-1][j-1] + 1 -
删除:
dp[i-1][j] + 1(删除word1[i-1]) -
插入:
dp[i][j-1] + 1(在word1后插入word2[j-1])
-
初始化:
-
dp[i][0] = i:将word1[0..i-1]转为空串需要删除i次 -
dp[0][j] = j:将空串转为word2[0..j-1]需要插入j次
结果 :dp[word1.length][word2.length]
优化思路(一维 DP 版):
class Solution {
public int minDistance(String word1, String word2) {
if (word2.length() > word1.length()) {
return minDistance(word2, word1);
}
int m = word1.length();
int n = word2.length();
int[] dp = new int[n + 1];
// 初始化:空串转 word2[0..j-1] 需要 j 次插入
for (int j = 0; j <= n; j++) {
dp[j] = j;
}
for (int i = 1; i <= m; i++) {
int prev = dp[0]; // 保存 dp[i-1][j-1]
dp[0] = i; // 新行第0列:word1[0..i-1] 转空串需要 i 次删除
for (int j = 1; j <= n; j++) {
int temp = dp[j];
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[j] = prev;
} else {
dp[j] = Math.min(Math.min(prev, dp[j]), dp[j - 1]) + 1;
}
prev = temp;
}
}
return dp[n];
}
}
