【LeetCode Hot100 刷题日记 (95/100)】72. 编辑距离——字符串、动态规划 (DP)🧠

5 阅读5分钟

📌 题目链接:72. 编辑距离 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:字符串、动态规划

⏱️ 目标时间复杂度:O(mn)

💾 空间复杂度:O(mn)


🧠 题目分析

本题要求计算将一个字符串 word1 转换为另一个字符串 word2 所需的最少操作次数,允许的操作有:

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

这类问题在自然语言处理(NLP)、拼写检查、生物信息学(如 DNA 序列比对)等领域有广泛应用。例如,在机器翻译系统中,编辑距离常被用作评估译文与参考译文相似程度的指标之一。

💡 关键观察
虽然题目提供了三种操作,但通过等价转换可以发现:

  • “删除 word1 中的一个字符” ≡ “在 word2 中插入一个字符”
  • “在 word1 中插入一个字符” ≡ “删除 word2 中的一个字符”
  • “替换 word1 的某个字符” ≡ “替换 word2 的对应字符”

因此,本质上只需考虑三种独立操作,并利用最优子结构重叠子问题特性,采用动态规划(Dynamic Programming, DP) 求解。


🧩 核心算法及代码讲解

✅ 动态规划思想详解

我们定义状态 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最小操作数。

📌 边界条件(Base Cases)

  • word1 为空(即 i = 0),要变成长度为 jword2,只能执行 j插入dp[0][j] = j
  • word2 为空(即 j = 0),要将长度为 iword1 变为空,只能执行 i删除dp[i][0] = i

🔄 状态转移方程

对于 i ≥ 1j ≥ 1,比较 word1[i-1]word2[j-1](注意下标从 0 开始):

  • 若字符相同:无需操作,直接继承 dp[i-1][j-1]
    dp[i][j] = dp[i-1][j-1];
    
  • 若字符不同:取以下三种操作的最小值 +1:
    1. 删除 word1[i-1] → 来自 dp[i-1][j] + 1
    2. 插入 word2[j-1]word1 末尾 → 来自 dp[i][j-1] + 1
    3. 替换 word1[i-1]word2[j-1] → 来自 dp[i-1][j-1] + 1

综上,统一写成:

int left = dp[i - 1][j] + 1;        // 删除 word1[i-1]
int down = dp[i][j - 1] + 1;        // 插入 word2[j-1]
int left_down = dp[i - 1][j - 1];   // 替换 or 不变
if (word1[i - 1] != word2[j - 1]) {
    left_down += 1;                 // 字符不同才需要替换
}
dp[i][j] = min({left, down, left_down});

💡 为什么只看末尾字符?
因为 DP 是按前缀构建的,所有操作都可以“推迟”到最后一步进行而不影响结果(操作顺序可交换)。这是 DP 能成立的关键前提。


🧭 解题思路(分步拆解)

  1. 初始化 DP 表:创建 (n+1) x (m+1) 的二维数组,其中 n = word1.length(), m = word2.length()
  2. 填充边界
    • 第一列为 0..n(全删除)
    • 第一行为 0..m(全插入)
  3. 逐行/列填表:从 (1,1) 开始,根据状态转移方程更新每个 dp[i][j]
  4. 返回结果dp[n][m] 即为答案。

示例验证(word1="horse", word2="ros")

ros
0123
h1123
o2212
r3222
s4332
e5443 ← 结果

📊 算法分析

项目内容
时间复杂度O(mn),双重循环遍历整个 DP 表
空间复杂度O(mn),存储 (n+1)x(m+1) 的 DP 表
优化方向可压缩为 O(min(m,n)) 空间(滚动数组),但面试中通常先写出标准二维 DP)
面试考点- 状态定义能力- 边界处理意识- 对“操作等价性”的理解- 能否解释为何 DP 有效

🎯 高频面试追问

  • 如果只允许插入和删除,怎么做?
  • 如何输出具体的编辑操作序列(而不仅是次数)?
  • 能否用记忆化搜索(递归+缓存)实现?

💻 代码

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    int minDistance(string word1, string word2) {
        int n = word1.length();
        int m = word2.length();

        // 有一个字符串为空串
        if (n * m == 0) return n + m;

        // DP 数组
        vector<vector<int>> D(n + 1, vector<int>(m + 1));

        // 边界状态初始化
        for (int i = 0; i < n + 1; i++) {
            D[i][0] = i;
        }
        for (int j = 0; j < m + 1; j++) {
            D[0][j] = j;
        }

        // 计算所有 DP 值
        for (int i = 1; i < n + 1; i++) {
            for (int j = 1; j < m + 1; j++) {
                int left = D[i - 1][j] + 1;        // 删除 word1[i-1]
                int down = D[i][j - 1] + 1;        // 在 word1 末尾插入 word2[j-1]
                int left_down = D[i - 1][j - 1];   // 替换或不变
                if (word1[i - 1] != word2[j - 1]) 
                    left_down += 1;                // 字符不同,需替换
                D[i][j] = min(left, min(down, left_down));
            }
        }
        return D[n][m];
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    // 示例 1
    cout << sol.minDistance("horse", "ros") << "\n";        // 输出: 3
    // 示例 2
    cout << sol.minDistance("intention", "execution") << "\n"; // 输出: 5
    // 边界测试
    cout << sol.minDistance("", "abc") << "\n";             // 输出: 3
    cout << sol.minDistance("abc", "") << "\n";             // 输出: 3
    cout << sol.minDistance("", "") << "\n";                // 输出: 0
    return 0;
}
/**
 * @param {string} word1
 * @param {string} word2
 * @return {number}
 */
var minDistance = function(word1, word2) {
    const n = word1.length;
    const m = word2.length;

    if (n * m === 0) return n + m;

    // 创建 (n+1) x (m+1) 的 DP 表
    const dp = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));

    // 初始化边界
    for (let i = 0; i <= n; i++) dp[i][0] = i;
    for (let j = 0; j <= m; j++) dp[0][j] = j;

    // 填表
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= m; j++) {
            const left = dp[i - 1][j] + 1;          // 删除
            const down = dp[i][j - 1] + 1;          // 插入
            let leftDown = dp[i - 1][j - 1];        // 替换或不变
            if (word1[i - 1] !== word2[j - 1]) {
                leftDown += 1;
            }
            dp[i][j] = Math.min(left, down, leftDown);
        }
    }

    return dp[n][m];
};

// 测试
console.log(minDistance("horse", "ros"));         // 3
console.log(minDistance("intention", "execution")); // 5
console.log(minDistance("", "abc"));              // 3
console.log(minDistance("abc", ""));              // 3
console.log(minDistance("", ""));                 // 0

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!