LeetCode 动态规划 72. 编辑距离 - 详解

编辑距离问题:动态规划详解与构建

问题描述

给定两个单词 word1word2,要求计算将 word1 转换为 word2 所需的最少操作数。允许的操作包括:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1
输入:word1 = "horse", word2 = "ros"
输出:3
解释:

  • horse -> rorse(替换 ‘h’ 为 ‘r’)
  • rorse -> rose(删除 ‘r’)
  • rose -> ros(删除 ‘e’)

示例 2
输入:word1 = "intention", word2 = "execution"
输出:5
解释:

  • intention -> inention(删除 ‘t’)
  • inention -> enention(替换 ‘i’ 为 ‘e’)
  • enention -> exention(替换 ‘n’ 为 ‘x’)
  • exention -> exection(替换 ‘n’ 为 ‘c’)
  • exection -> execution(插入 ‘u’)

难题核心在于:借助最小操作数实现字符串转换,每一步操作后,剩余部分形成子问题,适合用动态规划解决。


动态规划思路

动态规划的核心是将大问题分解为重叠子问题,并存储中间结果以避免重复计算。针对编辑距离,大家定义状态和转移方程如下。

状态定义
dp[i][j]dp[i][j]dp[i][j] 表示将 word1 的前 iii个字符(下标000i−1i-1i1)转换为 word2 的前 jjj个字符(下标000j−1j-1j1)所需的最少操作数。

  • iiijjj000 开始,iii 对应 word1 的长度,jjj 对应 word2 的长度。
  • 目标:计算 dp[n][m]dp[n][m]dp[n][m],其中 nnnmmm 分别为 word1word2 的长度。

初始状态

  • i=0i = 0i=0 时,word1 为空字符串,转换为 word2 的前 jjj 个字符需要 jjj次插入操作:dp[0][j]=jdp[0][j] = jdp[0][j]=j
  • j=0j = 0j=0 时,word2 为空字符串,word1 的前 iii 个字符需要 iii次删除操控:dp[i][0]=idp[i][0] = idp[i][0]=i
    数学表示为:
    dp[0][j]=j和dp[i][0]=i dp[0][j] = j \quad \text{和} \quad dp[i][0] = idp[0][j]=jdp[i][0]=i

状态转移方程
考虑 dp[i][j]dp[i][j]dp[i][j] 的计算,取决于 word1[i-1]word2[j-1] 是否相等:

  • 如果相等word1[i−1]=word2[j−1]word1[i-1] = word2[j-1]word1[i1]=word2[j1]):
    末尾字符相同,无需操作,直接继承子疑问结果:dp[i][j]=dp[i−1][j−1]dp[i][j] = dp[i-1][j-1]dp[i][j]=dp[i1][j1]
  • 如果不相等
    需要三种执行中的最小值:
    1. 插入:在 word1 末尾插入 word2[j-1],操作数加 111,剩余部分转换为dp[i][j−1]dp[i][j-1]dp[i][j1]dp[i][j]=dp[i][j−1]+1dp[i][j] = dp[i][j-1] + 1dp[i][j]=dp[i][j1]+1
    2. 删除:删除 word1[i-1],操作数加 111,剩余部分转换为dp[i−1][j]dp[i-1][j]dp[i1][j]dp[i][j]=dp[i−1][j]+1dp[i][j] = dp[i-1][j] + 1dp[i][j]=dp[i1][j]+1
    3. 替换:将 word1[i-1] 替换为 word2[j-1],操作数加 111,剩余部分转换为dp[i−1][j−1]dp[i-1][j-1]dp[i1][j1]dp[i][j]=dp[i−1][j−1]+1dp[i][j] = dp[i-1][j-1] + 1dp[i][j]=dp[i1][j1]+1
      综合为:dp[i][j]=min⁡(dp[i][j−1]+1,dp[i−1][j]+1,dp[i−1][j−1]+1)dp[i][j] = \min(dp[i][j-1] + 1, dp[i-1][j] + 1, dp[i-1][j-1] + 1)dp[i][j]=min(dp[i][j1]+1,dp[i1][j]+1,dp[i1][j1]+1)

完整状态转移方程用独立公式表示为:
dp[i][j]={dp[i−1][j−1]if word1[i−1]=word2[j−1]min⁡(dp[i−1][j]+1,dp[i][j−1]+1,dp[i−1][j−1]+1)otherwise dp[i][j] = \begin{cases} dp[i-1][j-1] & \text{if } word1[i-1] = word2[j-1] \\ \min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 1) & \text{otherwise} \end{cases}dp[i][j]={dp[i1][j1]min(dp[i1][j]+1,dp[i][j1]+1,dp[i1][j1]+1)if word1[i1]=word2[j1]otherwise

计算顺序
由于 dp[i][j]dp[i][j]dp[i][j]依赖于左上方数据(dp[i−1][j−1]dp[i-1][j-1]dp[i1][j1]dp[i−1][j]dp[i-1][j]dp[i1][j]dp[i][j−1]dp[i][j-1]dp[i][j1]),我们从上到下(iii111nnn)、从左到右(jjj111mmm)遍历二维数组,确保所有依赖值已计算。

示例推导(以 "horse""ros" 为例)

  • 初始化:dp[0][0]=0dp[0][0] = 0dp[0][0]=0(空转空),dp[i][0]=idp[i][0] = idp[i][0]=idp[0][j]=jdp[0][j] = jdp[0][j]=j
  • 计算 dp[1][1]dp[1][1]dp[1][1]word1[0]='h', word2[0]='r',不等,dp[1][1]=min⁡(dp[0][0]+1,dp[1][0]+1,dp[0][1]+1)=min⁡(1,1,1)=1dp[1][1] = \min(dp[0][0]+1, dp[1][0]+1, dp[0][1]+1) = \min(1, 1, 1) = 1dp[1][1]=min(dp[0][0]+1,dp[1][0]+1,dp[0][1]+1)=min(1,1,1)=1(替换运行)。
  • 类似步骤累积,最终dp[5][3]=3dp[5][3] = 3dp[5][3]=3

代码实现

以下为C++实现,代码简洁高效,包含详细注释。时间复杂度为O(nm)O(nm)O(nm),空间复杂度为O(nm)O(nm)O(nm),其中 nnnmmm为字符串长度。

#include <vector>
  #include <algorithm>
    using namespace std;
    class Solution {
    public:
    int minDistance(string word1, string word2) {
    int n = word1.size(); // word1 的长度
    int m = word2.size(); // word2 的长度
    // 创建 dp 数组,大小为 (n+1) x (m+1)
    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
      // 初始化边界条件
      for (int i = 0; i <= n; i++) {
      dp[i][0] = i; // word2 为空,需删除 i 个字符
      }
      for (int j = 0; j <= m; j++) {
      dp[0][j] = j; // word1 为空,需插入 j 个字符
      }
      // 填充 dp 数组
      for (int i = 1; i <= n; i++) {
      for (int j = 1; j <= m; j++) {
      if (word1[i - 1] == word2[j - 1]) {
      // 字符相等,直接继承子问题
      dp[i][j] = dp[i - 1][j - 1];
      } else {
      // 字符不等,取三种操作最小值
      dp[i][j] = min({dp[i - 1][j] + 1,     // 删除操作
      dp[i][j - 1] + 1,     // 插入操作
      dp[i - 1][j - 1] + 1}); // 替换操作
      }
      }
      }
      return dp[n][m]; // 返回最终结果
      }
      };

代码说明

  • 使用二维数组 dp 存储中间状态。
  • 双重循环遍历所有iiijjj,确保计算顺序正确。
  • min 函数(C++11 支持 min({...}))高效比较三种操作。

复杂度分析
  • 时间复杂度O(nm)O(nm)O(nm),其中 nnnmmm 分别为 word1word2 的长度。双重循环遍历整个 dpdpdp 数组。
  • 空间复杂度O(nm)O(nm)O(nm),用于存储 dpdpdp数组。可优化到O(min⁡(n,m))O(\min(n,m))O(min(n,m))使用滚动数组,但本文代码保持清晰性。

总结与扩展

本文详细解析了编辑距离问题的动态规划解法。核心在于状态定义dp[i][j]dp[i][j]dp[i][j]和转移方程,依据比较末尾字符分情况处理,确保最优子结构。该算法高效可靠,适用于大多数字符串转换场景。

优化建议

  • 空间优化:使用一维数组(滚动数组)可将空间复杂度降至O(min⁡(n,m))O(\min(n,m))O(min(n,m))
posted @ 2026-01-28 08:25  clnchanpin  阅读(0)  评论(0)    收藏  举报