编辑距离系列题

编辑距离的概念此处不再概述,下面开始说和编辑距离相关的一系列题目,主要包括:

  1. 普通的编辑距离
  2. 要求记录操作各操作的数目
  3. 只有删除操作的编辑距离
  4. 不同操作代价不同的编辑距离

1.编辑距离

思路:
使用动态规划,dp[i][j]表示word1[0...i]变成word2[0...j]的最短距离,举个栗子,假设word1=horseword2=ros,构造以下dp矩阵。

’ ’ros
’ ’0000
h0000
o0000
r0000
s0000
e0000

上述矩阵全初始化为0,矩阵的第一行表示由空字符串变成空字符串,r,ro,ros所需要的操作,可知对于word1来说由空字符串变成ros只能插入新的字符(只能修改word1),因此第一行可以依次填入0,1,2,3。即:

’ ’ros
’ ’0123
h0000
o0000
r0000
s0000
e0000

同理,第一列表示word1前面的若干字符变成空字符串所需的操作,则只能使用删除操作,依次填入操作次数。

’ ’ros
’ ’0123
h1000
o2000
r3000
s4000
e5000

可见在这个矩阵里,从左至右是插入操作,从上至下是删除操作,剩余的操作为替换,为什么要用替换呢,因为有时使用替换距离更短,比如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[i1][j]+1,dp[i][j1]+1,dp[i1][j1]+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矩阵如下:

’ ’ros
’ ’0123
h1123
o2212
r3222
s4332
e5443

时间复杂度 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.要求记录各操作数的编辑距离

要求记录各操作数的话,这里给两种思路:

  1. 在每一次判断时比较三个值,然后dp[i][j]不再存一个数,而是存一个数组,该数组存储当前位置的操作总数和各操作的数量;
  2. 先正常求编辑距离,然后回溯,根据前后的值判断选择了哪个操作。

这里有两个需要注意的地方:

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.只有删除操作的编辑距离

首先,需要说明的是,在这种情况下,word1word2都能删除。这个题目可以反向地想,先求两个字符串的最长公共子串的长度,然后使用两字符串的长度和减去两倍的最长公共子串的长度即可。
此处使用正向的思路,在这种情况下,从左至右和从上至下都是删除操作,但是从左至右为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[i1][j],dp[i][j1])+1dp[i1][j1]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[i1][j],dp[i][j1])dp[i1][j1]+1if word1[i] != word2[j]if word1[i] == word2[j]
可以看出有很好的对称关系。

4.不同操作代价不同的编辑距离

这个只需给不同的操作以不同的代价即可,用最普通的编辑距离方法,然后不同操作加不同的值,具体代码略。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值