最近看了一下别人的面试博客中提到了编辑距离的算法,才发现LeetCode中 这道题的难度已经从困难变成了中等,真是应了那句 "学如逆水行舟,不进则退" , 想当初一杯茶,一包烟,一道困难看一天,没想到如今能把这道题卷成中等
话不多说今天分享一下我对这道题的一些理解
bash
'' r o s
'' 0 1 2 3
h 1 1 2 3
o 2 2 1 2
r 3 2 2 2
s 4 3 3 2
e 5 4 4 3
先看一下这个矩阵 我们假设 word1 = 'horse'
word2 = 'ros'
第一行表示 从一个空字符串到 字符串 'ros' 所需要的最小步骤 也就是 3
第一列表示 从空字符串到 字符串 'horse' 所需要的最小步骤 也就是 5
每个单元格都表示 word1的前 i
个字符 转换成 word2 的前 j
个字符所需要的最小操作数
根据当前字符是否相等,来判断当前字符是否可以不需要操作,还是需要通过 替换 删除 插入来操作
可能说的有点抽象,举个例子来说
比如我想将 'horse' 的前两个字符 'ho' 转换成 'ros' 的前三个字符
首先,我们定义 dp[i][j]
为字符串 word1
的前 i
个字符和字符串 word2
的前 j
个字符之间的最小编辑距离
'ho' 转换成 'ros' ,在单元格中就是(2,3)
单元格,此单元格就是记录 dp[2][3]
的最小距离,值是2
那么 距离2 是怎么得来的呢
首先我们 假设 每个单元格都记录着word1 前 i 个字符转换成 word2 前 j 个字符的最小距离
什么意思呢 举个例子(1,1)
表示 'h' 转换成 'r' 的最小距离,(1,2)
表示 'h' 转换成 'ro' 的最小距离,
(1,3)
表示 'h' 转换成 'ros' 的最小距离,以此类推 (我们先假设这样,后面会验证)
现在我们想填充单元格 (2,3)
的值,我们首先需要判断 当前 word1的 'ho' 和 word2的 'ros' 的最后一个字符串是否相同 如果不相同,那就需要通过 替换 删除 插入
来操作word1
我们首先看一下 (2,2)
单元格,上面我们假设了每个单元格都记录着word1 前 i 个字符转换成 word2 前 j 个字符的最小距离,也就是说(2,2)
代表 'ho' 转换成 'ro' 的最小距离,可能有人会疑问为什么跟 (2,2)
有关系,因为(2,2)
转换成(2,3)
只需要在 'ho' 后面插入 's' 即可此时'ho'已经转换成'ro'
,也就是说根据 (2,2)
的值的基础上加一,就能将 'ho' 转换成 'ros'
同理我们看一下(1,3)
也就是 'h' 转换成 'ros' 的最小距离,在此基础上我们只需要删除 'ho' (此时'h'已经转换成'ros')
后面的 'o',也就是说根据 (1,3)
的值的基础上加一,就能将 'ho' 转换成 'ros'
再者,(1,2)
就是将 'ho' 的 'o' 替换成 'ros' 的 's', 根据 (1,2)
的值的基础上加一,就能将 'ho' 转换成 'ros'
这里我解释一下上面三个方法都要加一 是因为我们根据前一步的操作,再接上插入 删除 或替换,所以是在前一步的操作基础上加一
这三种方法都可以实现我们的需求,所以我们只需要选其中最小的一步即可获取最小距离,这个时候发现最小的值是 (2,2) + 1 = 2
,所以(2,3)
的最小值就是 2
刚才我们假设 每个单元格都记录着word1 前 i 个字符转换成 word2 前 j 个字符的最小距离
现在我们来验证一下
矩阵中的第一行第一列上面提到了,分别代表从一个空字符串到 字符串 'ros' 所需要的步骤和从空字符串到 字符串 'horse' 所需要的最少步骤
bash
'' r o s
'' 0 1 2 3
h 1
o 2
r 3
s 4
e 5
这个值是固定的,应该都能理解吧,现在我们要填充剩余的矩阵,那么按照上面我们的方法,当前单元格(i, j)
的最小值就是从 (i-1, i)
、(i, j-1)
、(i-1, j-1)
中取最小值在加一,我们所填充的每一步都是根据最小距离的基准来计算,所以我们填充的单元格一定是表示 当前 word1[i]
转换成 word2[j]
的最小距离
但是,其中还包含着一种特殊情况,就是当前字符相同的情况,什么意思呢,还是举例说明
我们在填充 (2, 2)
时,也就是将 'ho' 转换成 'ro', 发现 'ho' 的 'o' 与 'ro' 的 'o' 相同
bash
'' r o s
'' 0 1 2 3
h 1 1 2 3
o 2 2 ?
r 3
s 4
e 5
这个时候按照我们上面的方法,我们会取(1,1)
的值在加一 最后填充(2, 2)
的值为 2,但是结果并不如此,因为这种情况,我们可以直接继承 (1, 1)
的值,为什么呢,因为当前字符如果相同,我们可以不进行操作,我们只需要考虑将 'h' 转换成 'r' 即可,所以(2, 2)
的值 等于 (1, 1)
的值 也就是 1
至此,我们可以以最优解完整的填充整个矩阵,最小值也就是矩阵的右下角
动态规划通过填充一个矩阵来解决编辑距离问题,每个单元格的填充操作基于其相邻的三个单元格(左边、上边和左上角)
下面我用js代码,实现以上操作
js
function minDistance(word1, word2) {
const m = word1.length, n = word2.length;
//初始化dp数组(生成 m+1 行, n+1 列的矩阵)
const dp = Array.from({length: m + 1}, () => Array(n + 1).fill(0));
//初始化边界条件(可以理解为填充第一行第一列)
for(i = 0; i <= m; i++) {
dp[i][0] = i //填充第一列
}
for(j = 0; j <= n; j++) {
dp[0][j] = j //填充第一列
}
//填充矩阵
for(i = 0; i <= m; i++) {
for(j = 0; j <= n; j++) {
if(word1[i-1] === word2[j-1]) {
//如果相同 直接继承
dp[i][j] = dp[i-1][j-1]
}else {
//如果不同,从上一步操作中选取最小值,并 +1 填充
dp[i][j] = Math.min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1
}
}
}
return dp[m][n] //返回右下角的值(最小距离)
}