LeetCode 动态规划 72. 编辑距离 - 详解
编辑距离问题:动态规划详解与构建
问题描述
给定两个单词 word1 和 word2,要求计算将 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个字符(下标000 到 i−1i-1i−1)转换为 word2 的前 jjj个字符(下标000 到 j−1j-1j−1)所需的最少操作数。
- iii 和 jjj 从 000 开始,iii 对应
word1的长度,jjj 对应word2的长度。 - 目标:计算 dp[n][m]dp[n][m]dp[n][m],其中 nnn 和 mmm 分别为
word1和word2的长度。
初始状态:
- 当 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]=j和dp[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[i−1]=word2[j−1]):
末尾字符相同,无需操作,直接继承子疑问结果:dp[i][j]=dp[i−1][j−1]dp[i][j] = dp[i-1][j-1]dp[i][j]=dp[i−1][j−1]。 - 如果不相等:
需要三种执行中的最小值:- 插入:在
word1末尾插入word2[j-1],操作数加 111,剩余部分转换为dp[i][j−1]dp[i][j-1]dp[i][j−1]:dp[i][j]=dp[i][j−1]+1dp[i][j] = dp[i][j-1] + 1dp[i][j]=dp[i][j−1]+1。 - 删除:删除
word1[i-1],操作数加 111,剩余部分转换为dp[i−1][j]dp[i-1][j]dp[i−1][j]:dp[i][j]=dp[i−1][j]+1dp[i][j] = dp[i-1][j] + 1dp[i][j]=dp[i−1][j]+1。 - 替换:将
word1[i-1]替换为word2[j-1],操作数加 111,剩余部分转换为dp[i−1][j−1]dp[i-1][j-1]dp[i−1][j−1]:dp[i][j]=dp[i−1][j−1]+1dp[i][j] = dp[i-1][j-1] + 1dp[i][j]=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][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]={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[i−1][j−1]min(dp[i−1][j]+1,dp[i][j−1]+1,dp[i−1][j−1]+1)if word1[i−1]=word2[j−1]otherwise
计算顺序:
由于 dp[i][j]dp[i][j]dp[i][j]依赖于左上方数据(dp[i−1][j−1]dp[i-1][j-1]dp[i−1][j−1]、dp[i−1][j]dp[i-1][j]dp[i−1][j]、dp[i][j−1]dp[i][j-1]dp[i][j−1]),我们从上到下(iii 从 111 到 nnn)、从左到右(jjj 从 111 到 mmm)遍历二维数组,确保所有依赖值已计算。
示例推导(以 "horse" 转 "ros" 为例):
- 初始化:dp[0][0]=0dp[0][0] = 0dp[0][0]=0(空转空),dp[i][0]=idp[i][0] = idp[i][0]=i,dp[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),其中 nnn 和 mmm为字符串长度。
#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存储中间状态。 - 双重循环遍历所有iii 和 jjj,确保计算顺序正确。
min函数(C++11 支持min({...}))高效比较三种操作。
复杂度分析
- 时间复杂度:O(nm)O(nm)O(nm),其中 nnn 和 mmm 分别为
word1和word2的长度。双重循环遍历整个 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))。
浙公网安备 33010602011771号