Leetcode 72 编辑距离
本文层层递进,一共使用了五种方法,用Python解决了编辑距离问题。其根本思想是使用动态规划算法,根据状态转移方程即可求解。
题目再现
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
这个问题从直观上给人是很难的,但是在实际应用中非常常见。如下面的例子:输入法自动更正,当输入是左边的单词时,输入法会自动猜测到kitchen,而不是与实际输入差别很大的sitting。因此这种方法也能够衡量两个序列的相似度。例如DNA 序列是由 A,G,C,T 组成的序列,可以类比成字符串。编辑距离可以衡量两个 DNA 序列的相似度,编辑距离越小,说明这两段 DNA 越相似。
一 基本思想
1) 问题定义:编辑距离问题
Minimum Edit Distance, MED
输入
• 长度为𝒏的字符串𝒔,长度为𝒎的字符串𝒕
输出
• 求出一组编辑操作𝑶 =< 𝒆𝟏, 𝒆𝟐, … 𝒆𝒅 >,令
𝐦𝐢𝐧 |𝑶| ——优化目标
𝒔. 𝒕. 字符串𝒔经过𝑶的操作后满足𝒔 = 𝒕 ——约束条件
2)编辑操作
使用三种操作,即删除、插入、替换使得字符串𝒔变成字符串𝒕。
3) 递推公式
下面直接给出这个问题的递推公式,可以看出这与最长公共子序列的递推公式有类似之处。
二 代码实现
1) 递归法
这个方法的计算过程是自顶向上的,涉及了递归调用。
class Solution1: # 递归
def minEdietDistince(self, s1, s2):
def dp(i, j):
# base case
if i == -1: return j + 1
if j == -1: return i + 1
if s1[i] == s2[j]:
return dp(i-1, j-1) # 跳过
else:
return min(
dp(i, j-1) + 1, # 插入
dp(i-1, j) + 1, # 删除
dp(i-1, j-1) + 1 # 替换
)
return dp(len(s1)-1, len(s2)-1)
s1 = "intention"
s2 = "execution"
print(Solution1().minEdietDistince(s1, s2))
2) 动态规划优化——增加备忘录
在上面递归的基础上增加备忘录,可减少计算时间。
class Solution2: # 在前面的基础上增加备忘录,时间复杂度O(mn),空间复杂度O(mn)
def minEdietDistince(self, s1, s2):
meno = {} # 备忘录
def dp(i, j):
# base case
if i == -1: return j + 1
if j == -1: return i + 1
if (i, j) in meno:
return meno[(i, j)]
if s1[i] == s2[j]:
meno[(i, j)] = dp(i-1, j-1)
return dp(i-1, j-1) # 跳过
else:
meno[(i, j)] = min(
dp(i, j-1) + 1, # 插入
dp(i-1, j) + 1, # 删除
dp(i-1, j-1) + 1 # 替换
)
return meno[(i, j)]
return dp(len(s1)-1, len(s2)-1)
s1 = "intention"
s2 = "execution"
print(Solution2().minEdietDistince(s1, s2))
3) 动态规划优化——使用DP table
class Solution3: # DP table,时间复杂度O(mn),空间复杂度O(mn)
def minEdietDistince(self, s1, s2):
m, n = len(s1), len(s2)
if m * n == 0:
return m + n
dp = [[0]*(n+1) for _ in range(m+1)]
# base case
for i in range(m+1):
dp[i][0] = i
for j in range(n+1):
dp[0][j] = j
# 自底向上求解
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[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[m][n]
s1 = "intention"
s2 = "execution"
print(Solution3().minEdietDistince(s1, s2))
4) 一维DP table
参考文章
我们看到虽然dp是二维数组,但我们计算的时候每个元素只和他的左边,上边,左上角的3个值有关,所以这里我们还可以优化一下,使用一维数组,我们看下代码。
class Solution4: # DP table,时间复杂度O(mn),空间复杂度O(1)
def minEdietDistince(self, s1, s2):
m, n = len(s1), len(s2)
if m * n == 0:
return m + n
dp = [0] * (n+1)
for i in range(n+1):
dp[i] = i
last = 0 # 记录左上方
for i in range(1, m+1):
last = dp[0]
dp[0] = i
for j in range(1, n+1):
temp = dp[j]
if s1[i-1] == s2[j-1]:
dp[j] = last
else:
dp[j] = min(dp[j-1], dp[j], last) + 1
last = temp
return dp[n],dp
s1 = "intention"
s2 = "execution"
print(Solution4().minEdietDistince(s1, s2))
(5, [9, 8, 8, 8, 8, 8, 8, 7, 6, 5])
5) 增加追踪数组
最终返回最小操作次数(编辑距离)和操作记录数组
class Solution5: # DP table,时间复杂度O(mn),空间复杂度O(mn)
def minEdietDistince(self, s1, s2):
m, n = len(s1), len(s2)
if m * n == 0:
return m + n
dp = [[0]*(n+1) for _ in range(m+1)]
rec = [[0]*(n+1) for _ in range(m+1)]
# base case
for i in range(m+1):
dp[i][0] = i
rec[i][0] = "U"
for j in range(n+1):
dp[0][j] = j
rec[0][j] = "L"
# 自底向上求解
for i in range(1, m+1):
for j in range(1, n+1):
c = 0
if s1[i-1] != s2[j-1]:
c = 1
replace = dp[i-1][j-1] + c
delete = dp[i-1][j] + 1
insert = dp[i][j-1] + 1
if replace == min(replace, delete, insert):
dp[i][j] = dp[i-1][j-1] + c
rec[i][j] = "LU"
elif insert == min(replace, delete, insert):
dp[i][j] = dp[i][j-1] + 1
rec[i][j] = "L"
else:
dp[i][j] = dp[i-1][j] + 1
rec[i][j] = "U"
return dp[m][n],rec
```
# 最优方案追踪,输出编辑的方法
def trackback(rec, s1, s2, m, n):
if m == 0 or n == 0: return None
if rec[m][n] == "LU":
trackback(rec, s1, s2, m-1, n-1)
if s1[m-1] == s2[n-1]:
print("无需操作")
else:
print("将%s替换为%s"%(s1[m-1], s2[n-1]))
elif rec[m][n] == "U":
trackback(rec, s1, s2, m-1, n)
print("删除%s" % s1[m-1])
else:
trackback(rec, s1, s2, m, n-1)
print("插入%s" % s2[n-1])
#s1 = "intention"
#s2 = "execution"
s1 = 'horse'
s2 = 'ros'
m, n = len(s1), len(s2)
if __name__ == "__main__":
res, rec = Solution5().minEdietDistince(s1, s2)
print("将%s变为%s:" %(s1,s2))
print("将%s变为%s的编辑方法:" %(s1,s2))
print("操作次数(最小编辑距离):",res)
print("--------追踪数组--------")
for i in rec:
print(i)
trackback(rec, s1, s2, m, n)
下面是该方法的两个输入案例的结果:
将intention变为execution
操作次数(最小编辑距离): 5
--------追踪数组--------
[‘L’, ‘L’, ‘L’, ‘L’, ‘L’, ‘L’, ‘L’, ‘L’, ‘L’, ‘L’]
[‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘L’, ‘L’]
[‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’]
[‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘L’, ‘L’, ‘LU’]
[‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’]
[‘U’, ‘U’, ‘LU’, ‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’]
[‘U’, ‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘L’, ‘L’, ‘LU’]
[‘U’, ‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘L’, ‘L’]
[‘U’, ‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘U’, ‘LU’, ‘L’]
[‘U’, ‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘U’, ‘U’, ‘LU’]
将intention变为execution的编辑方法:
将i替换为e
将n替换为x
将t替换为e
将e替换为c
将n替换为u
无需操作
无需操作
无需操作
无需操作
将horse变为ros
操作次数(最小编辑距离): 3
--------追踪数组--------
[‘L’, ‘L’, ‘L’, ‘L’]
[‘U’, ‘LU’, ‘LU’, ‘LU’]
[‘U’, ‘LU’, ‘LU’, ‘L’]
[‘U’, ‘LU’, ‘U’, ‘LU’]
[‘U’, ‘U’, ‘LU’, ‘LU’]
[‘U’, ‘U’, ‘LU’, ‘U’]
将horse变为ros的编辑方法:
将h替换为r
无需操作
删除r
无需操作
删除e