最大公共字符串,最大公共子序列,编辑距离,myers等算法

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" 为例,lcs_{i,j} 为str1[0:i+1] 和 str2[0:j+1]的最大公共字符串。则递推如下:

下面开始写代码

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" 为例,lcs_{i,j} 为str1[0:i+1] 和 str2[0:j+1]的最大公共字符串。则递推如下:

下面开始写代码

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代表编辑距离,ED_{i,j}表示str1[:i]str2[:j]的编辑距离。下面来一行一行的解释:

 5.1   0==min(i,j)

        初始化,当i为0,或者j为0时,编辑距离就等于i和j中的最大值。

         5.1.1min(i,i)==0,必然有一个为空字符串,假设i==0,那么自然,从空字符串到str2[:j]需要j次插入操作

5.2   Others

        当 i不等于0且j不等于0时,选取三种操作 替换、删除 完成我们目标

       5.2.1 ED_{i-1,j-1}+d (d=0 if str1[i] == str2[j] else 1)这里说的是替换,用 str2[j]替换str1[i]时。而当str1[i]==str2[j]时,是不需要替换的,所以此时d=0。

        5.2.2  ED_{i-1,j}+1, ED_{i,j}相对于ED_{i-1,j}多了一个str1[i],对应的操作是删除。

        5.2.2  ED_{i,j-1}+1ED_{i,j}相对于ED_{i,j-1},少了一个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的方法,走快车道。

LCS,LCQ,ED的遍历方式
Myers的遍历方式

 6.2 举例说明Myers路径

        以str1 = "ZABCDZE" , str2="ABCDEF"为例,说明路径方式

        先来说一个目标,将str1通过约束的规则变为str2,但是要求操作次数最小,规则如下:

        只能有2种操作,1、删除 str1的某个字符;2、插入str2某个字符。或者保留二者相同的字符。且每个字符串中的每个字符无论是删除,插入,还是保留相同(滑滑梯操作),皆只能使用一次。而 操作次数=删除次数 + 插入次数。在图中,横向代表删除str1中的某个字符,纵向路径代表插入str2中某字符,而斜线代表保留相同的某字符。

myers路径

        红色箭头代表第一步,黄色箭头代表第二步,蓝色箭头代表第三步,所有的路径操作次数都是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的包生成的亮片对比文档,非手动标注!!!

        我们随便找了一段代码,随便修改了几个字母,看看效果。

Myers效果展示

      这样如果有人偷偷改了你的稿子,就一目了然了。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值