描述
给出两个单词word1
和word2
,计算出将word1
转换为word2
的最少操作次数。
你可进行三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
len(word1),len(word2)<=500len(word1),len(word2)<=500
样例
样例 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')
这一题的思想是用动态规划来做:
想法
编辑距离算法被数据科学家广泛应用,是用作机器翻译和语音识别评价标准的基本算法。
最直观的方法是暴力检查所有可能的编辑方法,取最短的一个。所有可能的编辑方法达到指数级,但我们不需要进行这么多计算,因为我们只需要找到距离最短的序列而不是所有可能的序列。
思路和算法
我们可以对任意一个单词进行三种操作:
-
插入一个字符;
-
删除一个字符;
-
替换一个字符。
题目给定了两个单词,设为 A
和 B
,这样我们就能够六种操作方法。
但我们可以发现,如果我们有单词 A
和单词 B
:
-
对单词
A
删除一个字符和对单词B
插入一个字符是等价的。例如当单词A
为doge
,单词B
为dog
时,我们既可以删除单词A
的最后一个字符e
,得到相同的dog
,也可以在单词B
末尾添加一个字符e
,得到相同的doge
; -
同理,对单词
B
删除一个字符和对单词A
插入一个字符也是等价的; -
对单词
A
替换一个字符和对单词B
替换一个字符是等价的。例如当单词A
为bat
,单词B
为cat
时,我们修改单词A
的第一个字母b -> c
,和修改单词B
的第一个字母c -> b
是等价的。
这样以来,本质不同的操作实际上只有三种:
-
在单词
A
中插入一个字符; -
在单词
B
中插入一个字符; -
修改单词
A
的一个字符。
这样以来,我们就可以把原问题转化为规模较小的子问题。我们用 A = horse
,B = ros
作为例子,来看一看是如何把这个问题转化为规模较小的若干子问题的。
-
在单词
A
中插入一个字符:如果我们知道horse
到ro
的编辑距离为a
,那么显然horse
到ros
的编辑距离不会超过a + 1
。这是因为我们可以在a
次操作后将horse
和ro
变为相同的字符串,只需要额外的1
次操作,在单词A
的末尾添加字符s
,就能在a + 1
次操作后将horse
和ro
变为相同的字符串; -
在单词
B
中插入一个字符:如果我们知道hors
到ros
的编辑距离为b
,那么显然horse
到ros
的编辑距离不会超过b + 1
,原因同上; -
修改单词
A
的一个字符:如果我们知道hors
到ro
的编辑距离为c
,那么显然horse
到ros
的编辑距离不会超过c + 1
,原因同上。
那么从 horse
变成 ros
的编辑距离应该为 min(a + 1, b + 1, c + 1)
。
注意:为什么我们总是在单词 A
和 B
的末尾插入或者修改字符,能不能在其它的地方进行操作呢?答案是可以的,但是我们知道,操作的顺序是不影响最终的结果的。例如对于单词 cat
,我们希望在 c
和 a
之间添加字符 d
并且将字符 t
修改为字符 b
,那么这两个操作无论为什么顺序,都会得到最终的结果 cdab
。
你可能觉得 horse
到 ro
这个问题也很难解决。但是没关系,我们可以继续用上面的方法拆分这个问题,对于这个问题拆分出来的所有子问题,我们也可以继续拆分,直到:
-
字符串
A
为空,如从 转换到ro
,显然编辑距离为字符串B
的长度,这里是2
; -
字符串
B
为空,如从horse
转换到 ,显然编辑距离为字符串A
的长度,这里是5
。
因此,我们就可以使用动态规划来解决这个问题了。我们用 D[i][j]
表示 A
的前 i
个字母和 B
的前 j
个字母之间的编辑距离。
当我们获得 D[i][j-1]
,D[i-1][j]
和 D[i-1][j-1]
的值之后就可以计算出 D[i][j]
。
-
D[i][j-1]
为A
的前i
个字符和B
的前j - 1
个字符编辑距离的子问题。即对于B
的第j
个字符,我们在A
的末尾添加了一个相同的字符,那么D[i][j]
最小可以为D[i][j-1] + 1
; -
D[i-1][j]
为A
的前i - 1
个字符和B
的前j
个字符编辑距离的子问题。即对于A
的第i
个字符,我们在B
的末尾添加了一个相同的字符,那么D[i][j]
最小可以为D[i-1][j] + 1
; -
D[i-1][j-1]
为A
前i - 1
个字符和B
的前j - 1
个字符编辑距离的子问题。即对于B
的第j
个字符,我们修改A
的第i
个字符使它们相同,那么D[i][j]
最小可以为D[i-1][j-1] + 1
。特别地,如果A
的第i
个字符和B
的第j
个字符原本就相同,那么我们实际上不需要进行修改操作。在这种情况下,D[i][j]
最小可以为D[i-1][j-1]
。
那么我们可以写出如下的状态转移方程:
-
若
A
和B
的最后一个字母相同:D[i][j]=min(D[i][j−1]+1,D[i−1][j]+1,D[i−1][j−1])=1+min(D[i][j−1],D[i−1][j],D[i−1][j−1]−1)D[i][j]=min(D[i][j−1]+1,D[i−1][j]+1,D[i−1][j−1])=1+min(D[i][j−1],D[i−1][j],D[i−1][j−1]−1)
-
若
A
和B
的最后一个字母不同:D[i][j]=1+min(D[i][j−1],D[i−1][j],D[i−1][j−1])D[i][j]=1+min(D[i][j−1],D[i−1][j],D[i−1][j−1])
所以每一步结果都将基于上一步的计算结果,示意如下:
对于边界情况,一个空串和一个非空串的编辑距离为 D[i][0] = i
和 D[0][j] = j
,D[i][0]
相当于对 word1
执行 i
次删除操作,D[0][j]
相当于对 word1
执行 j
次插入操作。
综上我们得到了算法的全部流程。
代码:
class Solution:
"""
@param word1: A string
@param word2: A string
@return: The minimum number of steps
"""
def min_distance(self, word1: str, word2: str) -> int:
# write your code here
if len(word1)==0 and len(word2)==0:
return 0
m,n=len(word1),len(word2)
dp= [[0 for i in range(n+1)] for j in range(m+1)]
for i in range(n+1):
dp[0][i]=i
for i in range(m + 1):
dp[i][0] = i
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]
word1 = "horse"
word2 = "ros"
sss = Solution()
print(sss.min_distance(word1,word2))