编辑距离的概念此处不再概述,下面开始说和编辑距离相关的一系列题目,主要包括:
- 普通的编辑距离
- 要求记录操作各操作的数目
- 只有删除操作的编辑距离
- 不同操作代价不同的编辑距离
1.编辑距离
思路:
使用动态规划,dp[i][j]
表示word1[0...i]
变成word2[0...j]
的最短距离,举个栗子,假设word1=horse
,word2=ros
,构造以下dp
矩阵。
’ ’ | r | o | s | |
---|---|---|---|---|
’ ’ | 0 | 0 | 0 | 0 |
h | 0 | 0 | 0 | 0 |
o | 0 | 0 | 0 | 0 |
r | 0 | 0 | 0 | 0 |
s | 0 | 0 | 0 | 0 |
e | 0 | 0 | 0 | 0 |
上述矩阵全初始化为0,矩阵的第一行表示由空字符串
变成空字符串
,r
,ro
,ros
所需要的操作,可知对于word1
来说由空字符串变成ros
只能插入
新的字符(只能修改word1
),因此第一行可以依次填入0
,1
,2
,3
。即:
’ ’ | r | o | s | |
---|---|---|---|---|
’ ’ | 0 | 1 | 2 | 3 |
h | 0 | 0 | 0 | 0 |
o | 0 | 0 | 0 | 0 |
r | 0 | 0 | 0 | 0 |
s | 0 | 0 | 0 | 0 |
e | 0 | 0 | 0 | 0 |
同理,第一列表示word1
前面的若干字符变成空字符串
所需的操作,则只能使用删除
操作,依次填入操作次数。
’ ’ | r | o | s | |
---|---|---|---|---|
’ ’ | 0 | 1 | 2 | 3 |
h | 1 | 0 | 0 | 0 |
o | 2 | 0 | 0 | 0 |
r | 3 | 0 | 0 | 0 |
s | 4 | 0 | 0 | 0 |
e | 5 | 0 | 0 | 0 |
可见在这个矩阵里,从左至右是插入
操作,从上至下是删除
操作,剩余的操作为替换
,为什么要用替换
呢,因为有时使用替换
距离更短,比如hello
->hollo
使用替换
只需一次操作,而使用其余操作则需两次。
从左至右用代码表示为:dp[i][j-1]
->dp[i][j]
,这个表示在已知由word1
的前i
个字符变成word2
的前j-1
个字符的情况下,怎么由word1
的前i
个字符变成word2
的前j
个字符,可知word1
直接插入word2
的第j
个字符即可。
同理,从上至下用代码表示为:dp[i-1][j]
->dp[i][j]
,则需删除word1
的第i
个字符。
现在考虑替换,假设已知dp[i-1][j-1]
,求dp[i][j]
,即已知由word1
的前i-1
个字符变成word2
的前j-1
个字符的情况下,求怎么由word1
的前i
个字符变成word2
的前j
个字符。此时需要考虑,word1[i]
是否等于word2[j]
,若相等,我们无需操作;若不想等,进行一次替换即可。
综上,转移方程为:
d
p
[
i
,
j
]
=
m
i
n
(
d
p
[
i
−
1
]
[
j
]
+
1
,
d
p
[
i
]
[
j
−
1
]
+
1
,
d
p
[
i
−
1
]
[
j
−
1
]
+
t
e
m
p
)
dp[i,j] = min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+temp)
dp[i,j]=min(dp[i−1][j]+1,dp[i][j−1]+1,dp[i−1][j−1]+temp)
t
e
m
p
=
0
i
f
w
o
r
d
1
[
i
]
=
=
w
o
r
d
2
[
j
]
e
l
s
e
1
temp = 0 \quad if \quad word1[i]== word2[j] \quad else \quad1
temp=0ifword1[i]==word2[j]else1
代码如下
def editDistance(word1,word2):
length1 = len(word1)
length2 = len(word2)
# 边界条件
if length1 == 0: return length2
if length2 == 0: return length1
dp = [[0 for j in range(length2+1)] for i in range(length1+1)]
# 依次填第一列和第一行
for i in range(1,length1+1):
dp[i][0] = i
for j in range(1,length2+1):
dp[0][j] = j
for i in range(1,length1+1):
for j in range(1,length2+1):
temp = 0 if word1[i-1] == word2[j-1] else 1
dp[i][j] = min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+temp)
return dp[length1][length2]
上例中完整dp
矩阵如下:
’ ’ | 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 |
时间复杂度
O
(
N
2
)
O(N^2)
O(N2),空间复杂度
O
(
N
2
)
O(N^2)
O(N2),空间复杂度可以进一步优化至O(N)
,观察代码可知,每次只需记录两行和第一列的数据,可以采用两个数组完成,具体如下代码所示。
def editDistance(word1,word2):
length1 = len(word1)
length2 = len(word2)
# 边界条件
if length1 == 0: return length2
if length2 == 0: return length1
last = [0]
# 把last设为第一行
for j in range(1,length2+1):
last.append(j)
for i in range(1,length1+1):
# 第一列的元素
current = [i]
for j in range(1,length2+1):
temp = 0 if word1[i-1] == word2[j-1] else 1
# last[j] 即dp[i-1][j] current[-1]即dp[i][j-1] last[j-1]即dp[i-1][j-1]
current.append(min(last[j]+1,current[-1]+1,last[j-1]+temp))
# 把last赋值为current
last = current
return current[-1]
2.要求记录各操作数的编辑距离
要求记录各操作数的话,这里给两种思路:
- 在每一次判断时比较三个值,然后
dp[i][j]
不再存一个数,而是存一个数组,该数组存储当前位置的操作总数和各操作的数量;- 先正常求编辑距离,然后回溯,根据前后的值判断选择了哪个操作。
这里有两个需要注意的地方:
1.当
temp == 0
时,替换操作数不需要加1
2.各操作数完全可能出现相同的情况,这时候就要有优先级了,在本文中优先级为:替换>删除>插入
思路1对应代码
def editDistance_RecordOperate(word1,word2):
length1 = len(word1)
length2 = len(word2)
# 边界条件
if length1 == 0: return length2
if length2 == 0: return length1
# dp元素 [total replace delete insert]
dp = [[[0,0,0,0] for j in range(length2+1)] for i in range(length1+1)]
# 初始化第一列
for i in range(1,length1+1):
dp[i][0][0] = i
# 第一列均为删除操作
dp[i][0][2] = i
for j in range(1,length2+1):
dp[0][j][0] = j
# 第一行均为插入操作
dp[0][j][3] = j
for i in range(1,length1+1):
for j in range(1,length2+1):
temp = 0 if word1[i-1] == word2[j-1] else 1
min_dis = min(dp[i-1][j][0]+1,dp[i][j-1][0]+1,dp[i-1][j-1]+temp)
dp[i][j][0] = min_dis
# 优先考虑替换
if min_dis == dp[i-1][j-1][0] + temp:
dp[i][j][1] = dp[i-1][j-1][1]
dp[i][j][2] = dp[i-1][j-1][2]
dp[i][j][3] = dp[i-1][j-1][3]
# 当temp == 1时替换操作数才需要加1
if temp == 1:
dp[i][j][1] += 1
# 其次删除
elif min_dis == dp[i-1][j][0]+1:
dp[i][j][1] = dp[i-1][j][1]
dp[i][j][2] = dp[i-1][j][2] + 1
dp[i][j][3] = dp[i-1][j][3]
# 最后插入
elif min_dis == dp[i][j-1][0]+1:
dp[i][j][1] = dp[i][j-1][1]
dp[i][j][2] = dp[i][j-1][2]
dp[i][j][3] = dp[i][j-1][3] + 1
(dis,replace,delete,insert) = dp[length1][length2]
print('%s -> %s'%(word1,word2))
print('distance:%2d'%dis)
print(' replace:%2d'%replace)
print(' delete:%2d'%delete)
print(' insert:%2d'%insert)
思路2代码:
def editDistance_BackTrack(word1,word2):
length1 = len(word1)
length2 = len(word2)
if length1 == 0: return length2
if length2 == 0: return length1
dp = [[0 for j in range(length2+1)] for i in range(length1+1)]
for i in range(1,length1+1):
dp[i][0] = i
for j in range(1,length2+1):
dp[0][j] = j
for i in range(1,length1+1):
for j in range(1,length2+1):
temp = 0 if word1[i-1] == word2[j-1] else 1
dp[i][j] = min(dp[i-1][j]+1,dp[i][j-1]+1,dp[i-1][j-1]+temp)
# 回溯
operate = [0,0,0]
def backtrack(row,col):
if row == 0 and col == 0:
return
temp = 0 if word1[i-1] == word2[j-1]
if dp[row][col] == dp[row-1][col-1] + temp:
if temp == 1:
operate[0] += 1
backtrack(row-1,col-1)
elif dp[row][col] == dp[row-1][col] + 1:
operate[1] += 1
backtrack(row-1,col)
elif dp[row][col] == dp[row][col-1] + 1:
operate[2] += 1
backtrack(row,col-1)
# 开始回溯
backtrack(length1,length2)
# 打印信息
print('%s -> %s'%(word1,word2))
print('distance:%2d'%dp[length1][length2])
print(' replace:%2d'%operate[0])
print(' delete:%2d'%operate[1])
print(' insert:%2d'%operate[2])
注意,优先级的先后会影响结果!第二种思路的空间复杂度更低,但是相对地要多运行一次回溯。
3.只有删除操作的编辑距离
首先,需要说明的是,在这种情况下,word1
和word2
都能删除。这个题目可以反向地想,先求两个字符串的最长公共子串的长度,然后使用两字符串的长度和减去两倍的最长公共子串的长度即可。
此处使用正向的思路,在这种情况下,从左至右和从上至下都是删除操作,但是从左至右为word2
删除,而从上至下为word1
删除(这个说法其实不严谨,但是姑且这么说吧)。现在考虑转移方程,
dp[i-1][j]
->dp[i][j]
表示的是word1[0...i-1]
和word2[0..j]
经删除操作变为相同的字符串(用temp
代替)的操作数,现在考虑在这个情况下,word1[0...i]
怎么变成temp
,可见,只需删除word1[i]
即可,故dp[i][j] = dp[i-1][j]+1
。
同理,dp[i][j-1]
->dp[i][j]
只需删除word2[j]
即可,故dp[i][j] = dp[i][j-1]+1
。
再考虑dp[i-1][j-1]
->dp[i][j]
,若word1[i]==word2[j]
,无需任何操作;若不相等,退化为上述两种情况。
故
d p [ i ] [ j ] = { m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + 1 if word1[i] != word2[j] d p [ i − 1 ] [ j − 1 ] if word1[i] == word2[j] dp[i][j]= \begin{cases} min(dp[i-1][j],dp[i][j-1])+1& \text{if word1[i] != word2[j]}\\ dp[i-1][j-1]& \text{if word1[i] == word2[j]} \end{cases} dp[i][j]={min(dp[i−1][j],dp[i][j−1])+1dp[i−1][j−1]if word1[i] != word2[j]if word1[i] == word2[j]
代码
def editDistanceOnlyDelete(word1,word2):
length1 = len(word1)
length2 = len(word2)
if length1 == 0: return length2
if length2 == 0: return length1
dp = [[0 for j in range(1+length2)] for i in range(1+length1)]
for i in range(1,length1+1):
dp[i][0] = i
for j in range(1,length2+1):
dp[0][j] = j
for i in range(1,length1+1):
for j in range(1,length2+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],dp[i][j-1])+1
return dp[length1][length2]
对比这个转移方程,最大公共子串的转移方程为:
d
p
[
i
]
[
j
]
=
{
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
)
if word1[i] != word2[j]
d
p
[
i
−
1
]
[
j
−
1
]
+
1
if word1[i] == word2[j]
dp[i][j]= \begin{cases} max(dp[i-1][j],dp[i][j-1])& \text{if word1[i] != word2[j]}\\ dp[i-1][j-1]+1& \text{if word1[i] == word2[j]} \end{cases}
dp[i][j]={max(dp[i−1][j],dp[i][j−1])dp[i−1][j−1]+1if word1[i] != word2[j]if word1[i] == word2[j]
可以看出有很好的对称关系。
4.不同操作代价不同的编辑距离
这个只需给不同的操作以不同的代价即可,用最普通的编辑距离方法,然后不同操作加不同的值,具体代码略。