1 前言
这个4个算法比较相似,并且有以下相同点和不同点
2 异同点
以str1 = "ABCDEF" , str2="ZABCDZE" 为例
相同点:
1、都是在字符串上得到某个目标;2、算法的核心都是动态规划的思想。
不同点:
1、目标不同,其中最大公共字符串是最大连续的子序列,例如:最大公共字符串是"ABCD" ,长度为4。而最大公共子序列是"ABCDE",长度为5。
2、编辑距离,是求从一个字符串str1到另一个字符串str2的变动的最小次数,其中变动只在一个字符串上发生,变动包括三个动作:删除,插入,更改。
3、Myers可能听得比较少,但是作为程序员应该都用过,因为SVN和GIT的版本比对算法,diff是用的这个算法,他可以比较全面的寻找到每一个节点的异同。虽然是动态规划,但是它跟前面的三个算法的遍历方式不同,这也是本质的不同,这里提一下,后面详述。补充一句,对于
不废话了,下面来直接看这三个的实现过程。
3 最大公共字符串(LCS)
最大公共字符串是最大的连续的公共的字符串的长度,既然是动态规划,必然是要有递推式。先写出来。
以str1 = "ABCDEF" , str2="ZABCDZE" 为例, 为
和
的最大公共字符串。则递推如下:
下面开始写代码
def longer_common_string(str1, str2):
"""
最大公共字符串 实现
"""
len1 = len(str1)
len2 = len(str2)
max_lcs_len = 0
max_len_axis = (0, 0)
lcs_matrix = [[0 for j in range(len2+1)] for i in range(len1+1)]
for i, char_1 in enumerate(str1):
for j, char_2 in enumerate(str2):
if char_1 == char_2:
lcs_matrix[i+1][j+1] = lcs_matrix[i][j] + 1
if lcs_matrix[i+1][j+1] > max_lcs_len:
max_lcs_len = lcs_matrix[i+1][j+1]
max_len_axis = (i, j)
else:
lcs_matrix[i+1][j+1] = 0
return max_lcs_len, max_len_axis
str1 = "ABCDEF"
str2 = "ZABCDZE"
lcs_len, axis = longer_common_string(str1, str2)
print(lcs_len)
print(axis)
# print result
# 4
# (3, 4)
得到结果 lcs_len = 4, axis =(3,4),表示最大公共子序列在str1 索引为3的位置结束,在str2索引为4的地方结束。
4 最大公共子序列(LCQ)
子序列可以是非连续的字符串,因此 lcq >= lcs 恒成立。对于递推关系,则要所有改变了, 以str1 = "ABCDEF" , str2="ZABCDZE" 为例, 为
和
的最大公共字符串。则递推如下:
下面开始写代码
def longer_common_sequence(str1, str2):
"""
最大公共子序列 实现
"""
len1 = len(str1)
len2 = len(str2)
max_lcq_len = 0
max_len_axis = (0, 0)
lcq_matrix = [[0 for j in range(len2+1)] for i in range(len1+1)]
for i, char_1 in enumerate(str1):
for j, char_2 in enumerate(str2):
if char_1 == char_2:
lcq_matrix[i+1][j+1] = lcq_matrix[i][j] + 1
if lcq_matrix[i+1][j+1] > max_lcq_len:
max_lcq_len = lcq_matrix[i+1][j+1]
max_len_axis = (i, j)
else:
lcq_matrix[i+1][j+1] = max(lcq_matrix[i+1][j], lcq_matrix[i][j+1])
return max_lcq_len, max_len_axis
str1 = "ABCDEF"
str2 = "ZABCDZE"
lcq_len, axis = longer_common_sequence(str1, str2)
print(lcq_len)
print(axis)
# print result
# 5
# (4, 6)
结果意思与LCS相同,不再赘述。
5 编辑距离Edit Distance
目标:使得str1通过 替换、插入,删除 三种操作,以最小的操作数,变为str2
编辑距离与LCS,LCQ不同之处在在于,编辑距离是找不同,前两者是找相同,在某种意义上也是殊途同归。
先上递推公式,再解释
ED代表编辑距离,表示
和
的编辑距离。下面来一行一行的解释:
5.1 0==min(i,j)
初始化,当i为0,或者j为0时,编辑距离就等于i和j中的最大值。
5.1.1 当,必然有一个为空字符串,假设
,那么自然,从空字符串到
需要
次插入操作
5.2 Others
当 i不等于0且j不等于0时,选取三种操作 替换、删除 完成我们目标
5.2.1 这里说的是替换,用 str2[j]替换str1[i]时。而当str1[i]==str2[j]时,是不需要替换的,所以此时d=0。
5.2.2 ,
相对于
多了一个str1[i],对应的操作是删除。
5.2.2 ,
相对于
,少了一个str2[j],对应操作是增加。
最后再从 增加,删除,插入 三个操作中取得最小值。
代码如下:
def edit_distance(str1, str2):
len1 = len(str1)
len2 = len(str2)
ed_matrix = [[max(i, j) if 0 == min(i,j) else 0 for j in range(len2+1)] for i in range(len1+1)]
for i, char_1 in enumerate(str1):
for j, char_2 in enumerate(str2):
if char_1 == char_2:
d = 0
else:
d = 1
replace_dist = ed_matrix[i][j] + d
insert_dist = ed_matrix[i-1][j] + 1
delete_dist = ed_matrix[i][j-1] + 1
ed_matrix[i+1][j+1] = min(replace_dist, insert_dist, delete_dist)
return ed_matrix[-1][-1]
str1 = "ABCDEF"
str2 = "ZABCDZE"
ed = edit_distance(str1, str2)
print(ed)
# print
# 3
终于把上面的三款砖抛完了,该引出玉了
6、Myers
6.1 遍历方式不同
上面三种的动态规划,都是以 x,y轴作为遍历方便的,而Myers是以 x+y 和 x-y的方向上做遍历的,这样有个好处,就是在碰到连续的 str1[i]==str2[j]可以以时间复杂度为1的方法,走快车道。
![](https://i-blog.csdnimg.cn/blog_migrate/d3c0a066224b19bd62e105688262ed22.png)
![](https://i-blog.csdnimg.cn/blog_migrate/1309740e23019602d011f8e724d361fe.png)
6.2 举例说明Myers路径
以str1 = "ZABCDZE" , str2="ABCDEF"为例,说明路径方式
先来说一个目标,将str1通过约束的规则变为str2,但是要求操作次数最小,规则如下:
只能有2种操作,1、删除 str1的某个字符;2、插入str2某个字符。或者保留二者相同的字符。且每个字符串中的每个字符无论是删除,插入,还是保留相同(滑滑梯操作),皆只能使用一次。而 操作次数=删除次数 + 插入次数。在图中,横向代表删除str1中的某个字符,纵向路径代表插入str2中某字符,而斜线代表保留相同的某字符。
![](https://i-blog.csdnimg.cn/blog_migrate/869bf741e20836fef6d8bdab0f0b037d.png)
红色箭头代表第一步,黄色箭头代表第二步,蓝色箭头代表第三步,所有的路径操作次数都是3,本来2^3=8一共八条路径,由于其中有一条超出界外了,所以一共7条,我们现在来依次看看八条路是怎么走的,为什么有的快,有的慢。
第1条路径:[(0,0), (4,5), (5,7),(6,7)]
第2条路径:[(0,0), (4,5),(5,5),(5,6)]
第3条路径:[(0,0), (4,5),(5,5),(6,5)]
第4条路径:[(0,0), (1,0),(1,1),(4,5)]
第5条路径:[(0,0), (1,0),(1,1),(2,1)]
第6条路径:[(0,0), (1,0),(2,0),(2,1)]
第7条路径:[(0,0), (1,0),(2,0),(3,0)]
这里只解释第一条路径,其6条大家自己琢磨
step1:红色箭头 先删除Z: str1[0],,来到坐标(0,1)。这时候发现 A:str1[1]==str2[0] && B:str1[2]==str2[1] && C:str1[3]==str2[2] && D:str1[4]==str2[3],因此,坐标(0,1)走滑滑梯直接到了(4,5),
step2 第二步,黄色剪头,删除Z: str1[5], 来到坐标 (4,6),删除后发现E:str1[6]==str2[4],因此坐标(4,6)也坐滑滑梯来到坐标(5,7)。
step3 最后一步,蓝色箭头,插入F:str2[5],,来到坐标(6,7).
完成从str1,到str2的变化。一共用了3步操作(滑滑梯不算)。走的步数是小于 3*8=24步,因为有些线路的前期路径是公共的,一共走了13步(时间复杂度),实际在代码中只会走7步,还要减去6步,因为第一条路径达到终点时,循环已经结束了,其它6条最后一步不用走了,我们只要冠军。而最大公共字符串(字符列),编辑距离时间复杂度是6*7=42步。
6.3 代码部分
代码主要是三个部分,1、构建全部路径图;2、回溯出最优的路径;3、依靠路径找出两个序列间的关系。
代码只放部分,其余的感兴趣在评论区找我。
def main(self):
# 1、构造全路径图
target_node = self._construct_graph()
# 2、获取最优路径
path_list = self._get_path(target_node)
print(path_list)
# 3、路径转化为关系
all_relation = self._get_relation(path_list)
print("====="*10)
for relation in all_relation:
print(relation)
if __name__ == '__main__':
s1 = "ZABCDZE"
s2 = "ABCDEF"
myers = Myers(s1, s2)
myers.main()
# [(0, 0), (4, 5), (5, 7), (6, 7)]
# ==================================================
# [('delete', 0), ('common', 1, 0), ('common', 2, 1), ('common', 3, 2), ('common', 4, 3)]
# [('delete', 5), ('common', 6, 4)]
# [('insert', 5)]
· 解释一下输出(print)
1、路径:[(0, 0), (4, 5), (5, 7), (6, 7)]
与Myers的路径图是一致的,与其中的最先到达终点的最优路径图是一致的。
2、关系:
[('delete', 0), ('common', 1, 0), ('common', 2, 1), ('common', 3, 2), ('common', 4, 3)]
[('delete', 5), ('common', 6, 4)]
[('insert', 5)]
一共三组关系,解释一下是什么意思
回顾目标,使得str1 = "ZABCDZE" ,通过约束规则变为str2="ABCDEF"
第一组:删除Z:str1[0],保留公共的A:str1[1]==str2[0] && B:str1[2]==str2[1] && C:str1[3]==str2[2] && D:str1[4]==str2[3],这时得到是 ABCD。
第二组;删除Z:str1[5], 保留公共的E:str1[6]==str2[4],得到 ABCDE
第三组;插入F:str2[5],得到 ABCDEF
3、得到这个关系后的作用,我们可以从这个关系得到最大公共字符串,最大公共子序列,以及编剧距离的值。
最大公共字符串:连续common的个数,直接数:4
最大公共序列,所有common的个数,直接数:5
编辑距离,每一组中,取插入和删除得最大值,然后相加: 1+1+1=3
一个速度比它三都快,结果完全可以转换成其中的任意的一个值。一个字评价:完美。
6.4 作用
它的作用在SVN,GIT的版本管理工具中的文本比对中会有用到。我们也可以利用word文档自动标记,来将两段相似的文本的不同之处,高亮显示。这是用 python-dcox的包生成的亮片对比文档,非手动标注!!!
我们随便找了一段代码,随便修改了几个字母,看看效果。
![](https://i-blog.csdnimg.cn/blog_migrate/ca16235049e86344f82ca68e91780401.png)
这样如果有人偷偷改了你的稿子,就一目了然了。