LeetCode 热题 100 之 64. 最小路径和 5. 最长回文子串 1143. 最长公共子序列 72. 编辑距离

本次题目为以下四题

  1. 最小路径和

  2. 最长回文子串

  3. 最长公共子序列

  4. 编辑距离

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 中从索引 ij 的子串是否为回文

  • 状态转移:

    • i == j(单个字符):dp[i][j] = true

    • j - i == 1(两个字符):dp[i][j] = (s[i] == s[j])

    • j - i > 1dp[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):

  1. 预处理字符串:在每个字符间插入特殊符号(如 #),统一奇偶长度回文的处理(例:babad#b#a#b#a#d#

  2. 定义关键变量:

    1. p[i]:以 i 为中心的最长回文半径(包含自身)

    2. center:当前最长回文的中心

    3. right:当前最长回文的右边界

  3. 利用对称性快速计算 p[i],仅在必要时扩展边界

代码解释

  • 预处理:插入 # 后,所有回文都是奇数长度(如 bb#b#b#,中心在第二个 #

  • 对称性优化:i < right 时,p[i] 先取对称点的半径或 right - i(避免越界)

  • 扩展:仅在必要时向外扩展,减少重复比较

  • 还原:通过 maxCentermaxLen 计算原字符串的起始索引,截取结果

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] = 0dp[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];
}
}
相关推荐
CoderCodingNo2 小时前
【GESP】C++五级练习题 luogu-P1182 数列分段 Section II
开发语言·c++·算法
放下华子我只抽RuiKe52 小时前
机器学习全景指南-直觉篇——基于距离的 K-近邻 (KNN) 算法
人工智能·gpt·算法·机器学习·语言模型·chatgpt·ai编程
kisshuan123962 小时前
[特殊字符]【深度学习】DA3METRIC-LARGE单目深度估计算法详解
人工智能·深度学习·算法
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章33-Blod分析
图像处理·人工智能·opencv·算法·计算机视觉
Eward-an3 小时前
LeetCode 239. 滑动窗口最大值(详细技术解析)
python·算法·leetcode
一叶落4383 小时前
LeetCode 50. Pow(x, n)(快速幂详解 | C语言实现)
c语言·算法·leetcode
皙然3 小时前
彻底吃透红黑树
数据结构·算法
t198751284 小时前
TOA定位算法MATLAB实现(二维三维场景)
开发语言·算法·matlab
jllllyuz4 小时前
粒子群算法解决资源分配问题的MATLAB实现
开发语言·算法·matlab