LeetCode 第72题:编辑距离

93 阅读5分钟

LeetCode 第72题:编辑距离

题目描述

给你两个单词 word1word2,请返回将 word1 转换成 word2 所使用的最少操作数。

你可以对一个单词进行如下三种操作:

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

难度

困难

题目链接

点击在LeetCode中查看题目

示例

示例 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')

提示

  • 0 <= word1.length, word2.length <= 500
  • word1word2 由小写英文字母组成

解题思路

动态规划

这道题是经典的编辑距离问题,最适合使用动态规划来解决。

关键点:

  1. 定义状态:dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作数
  2. 状态转移方程:
    • 如果 word1[i-1] == word2[j-1],则 dp[i][j] = dp[i-1][j-1](不需要操作)
    • 否则,dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
      • dp[i-1][j-1] + 1 表示替换操作
      • dp[i-1][j] + 1 表示删除操作
      • dp[i][j-1] + 1 表示插入操作
  3. 初始化:
    • dp[i][0] = i(将 word1 的前 i 个字符全部删除)
    • dp[0][j] = j(插入 word2 的前 j 个字符)
  4. 最终结果:dp[m][n],其中 mn 分别是 word1word2 的长度

具体步骤:

  1. 初始化 dp 数组
  2. 填充 dp 数组
  3. 返回 dp[m][n]

图解思路

算法步骤分析表

以 word1 = "horse", word2 = "ros" 为例:

步骤操作dp数组说明
初始初始化dp[0][0]=0, dp[i][0]=i, dp[0][j]=j边界情况
遍历(1,1)dp[1][1]=1h!=r,取min(0,1,1)+1=1
遍历(1,2)dp[1][2]=2h!=o,取min(1,1,2)+1=2
遍历(1,3)dp[1][3]=3h!=s,取min(2,2,3)+1=3
遍历(2,1)dp[2][1]=2o!=r,取min(1,2,1)+1=2
遍历(2,2)dp[2][2]=1o==o,取dp[1][1]=1
............
最终结果dp[5][3]=3最少需要3步

状态/情况分析表

情况输入输出说明
相同字符串"abc", "abc"0不需要任何操作
一个为空"", "abc"3需要插入所有字符
完全不同"abc", "def"3需要替换所有字符
部分相同"horse", "ros"3需要多种操作组合

代码实现

C# 实现

public class Solution {
    public int MinDistance(string word1, string word2) {
        int m = word1.Length;
        int n = word2.Length;
        
        // 如果有一个字符串为空,则编辑距离为另一个字符串的长度
        if (m == 0) return n;
        if (n == 0) return m;
        
        // 创建dp数组
        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[i - 1] == word2[j - 1]) {
                    // 如果当前字符相同,不需要操作
                    dp[i, j] = dp[i - 1, j - 1];
                } else {
                    // 取替换、删除、插入三种操作的最小值
                    dp[i, j] = Math.Min(dp[i - 1, j - 1], Math.Min(dp[i - 1, j], dp[i, j - 1])) + 1;
                }
            }
        }
        
        return dp[m, n];
    }
}

Python 实现

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m, n = len(word1), len(word2)
        
        # 如果有一个字符串为空,则编辑距离为另一个字符串的长度
        if m == 0:
            return n
        if n == 0:
            return m
        
        # 创建dp数组
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        
        # 初始化边界
        for i in range(m + 1):
            dp[i][0] = i
        for j in range(n + 1):
            dp[0][j] = j
        
        # 填充dp数组
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                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 - 1][j], dp[i][j - 1]) + 1
        
        return dp[m][n]

C++ 实现

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.length();
        int n = word2.length();
        
        // 如果有一个字符串为空,则编辑距离为另一个字符串的长度
        if (m == 0) return n;
        if (n == 0) return m;
        
        // 创建dp数组
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        
        // 初始化边界
        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[i - 1] == word2[j - 1]) {
                    // 如果当前字符相同,不需要操作
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    // 取替换、删除、插入三种操作的最小值
                    dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
                }
            }
        }
        
        return dp[m][n];
    }
};

执行结果

  • C# 执行用时:84 ms
  • C# 内存消耗:39.2 MB
  • Python 执行用时:160 ms
  • Python 内存消耗:17.8 MB
  • C++ 执行用时:12 ms
  • C++ 内存消耗:9.1 MB

代码亮点

  1. 🎯 使用动态规划高效解决问题
  2. 💡 清晰的状态定义和转移方程
  3. 🔍 处理边界情况(空字符串)
  4. 🎨 代码结构清晰,易于理解

常见错误分析

  1. 🚫 状态转移方程错误
  2. 🚫 边界条件处理不当
  3. 🚫 没有考虑空字符串的情况
  4. 🚫 混淆插入和删除操作的含义

解法对比

解法时间复杂度空间复杂度优点缺点
递归(暴力)O(3^n)O(n)直观易懂效率极低,会超时
记忆化递归O(m*n)O(m*n)相对简单递归栈可能溢出
动态规划O(m*n)O(m*n)高效稳定需要额外空间
空间优化DPO(m*n)O(min(m,n))节省空间实现复杂

相关题目