最长公共子序列问题Python实现

最长公共子序列问题

问题介绍

最长公共子序列(LCS)是一个在一个序列集合中(通常为两个序列)用来查找所有序列中最长子序列的问题。一个数列 ,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则称为已知序列的最长公共子序列 —— [ 百度百科 ]

本文使用动态规划技术算法时间复杂度为Θ(mn),优于蛮力搜索的时间复杂度为Θ(m2n)。

使用动态规划技术,首先要寻找一个递推公式,令序列A=a1a2…an和序列B=b1b2…bn。令L[i,j]表示为a1a2…ai和b1b2…bj的最长公共子序列的长度。因为i和j可能为0,即a1a2…ai和b1b2…bj其中一个或同时为空的序列。

得到以下结论:

  1. 如果i=0或j=0,那么L[i,j]=0;
  2. 如果i和j都大于0,那么
    • 如果ai=bi,L[i,j]=L[i-1,j-1]+1;
    • 如果ai≠bi,L[i,j]=max(L[i,j-1],L[i-1,j])。

递推式

L(i,j)=0,L[i1,j1]+1,max(L[i,j1],L[i1,j]i=0 j=0i>0, j>0ai=bji>0, j>0ai bj


伪代码

for i ← 0 to m
    for j ← 0 to n
        L[i,j] ← 0
end for
for i ← 1 to m
    for j ← 1 to n
        if a[i] = b[j]
            then L[i,j] ← L[i-1,j-1]+1
        else
            then L[i,j] ← max{L[i,j-1],L[i-1,j]}
        end if
    end for
end for
return L[m,n]

使用Python实现,并通过标记列表以自底向下的方式计算最长子序列的最优解(两个序列在递推时最先达的最长公共子序列)

# coding = utf-8

class LCS:
    """
    两个序列求其最长子序列
    """
    def __init__(self, str1, str2):
        self.str1 = str1
        self.str2 = str2
        self.str_lcs = ''

    def get_flag(self):
        """
        通过动态规划的方式生成结果矩阵和标记矩阵
        :return: 计算表和标记表
        """
        str1_len = self.str1.__len__()  # 获取字符串的长度
        str2_len = self.str2.__len__()

        chart = [[0 for col in range(str2_len + 1)]
                 for row in range(str1_len + 1)]  # 初始化二维列表,并将其值全部置为0
        chart_flag = [['--' for col in range(str2_len + 1)]
                      for row in range(str1_len + 1)]  # 初始化二维列表,并将其值全部置为--
        for col in range(1, chart_flag[0].__len__()):  # 初始化标记列表的第一行和第一列为字符串a,b中字符
            chart_flag[0][col] = ' ' + self.str2[col - 1]
        for row in range(1, chart_flag.__len__()):
            chart_flag[row][0] = ' ' + self.str1[row - 1]

        for row in range(str1_len):
            for col in range(str2_len):
                """
                如果a[row-1]=b[col-1],则a[:row]和b[:col]的最长子序列长度为a[:row-1]和b[:col-1]的最长子序列长度加一
                如果a[row-1]!=b[col-1],则a[:row]和b[:col]的最长子序列长度为{{a[:row-1],b[:col]},{a[:row],b[:col-1]|最长子序列长度的最大值}
                """
                if self.str1[row] == self.str2[col]:
                    chart[row + 1][col + 1] = chart[row][col] + 1
                    chart_flag[row + 1][col + 1] = '↖'
                else:
                    chart[row + 1][col + 1] = max(chart[row + 1][col], chart[row][col + 1])
                    if chart[row + 1][col + 1] == chart[row][col + 1]:
                        chart_flag[row + 1][col + 1] = '↑'
                    else:
                        chart_flag[row + 1][col + 1] = '←'

        return chart, chart_flag

    def get_cls(self, flag, row, col):
        """
        利用标记矩阵自底向上来求最优解
        :param flag: 标记列表
        :param row: 第一个子序列的长度
        :param col: 第二个子序列的长度
        :return:
        """
        if 0 == row or 0 == col:
            return
        if flag[row][col] == '↖':
            self.get_cls(flag, row - 1, col - 1)
            self.str_lcs = self.str_lcs + self.str1[row - 1]
        elif flag[row][col] == '←':
            self.get_cls(flag, row, col - 1)
        else:
            self.get_cls(flag, row - 1, col)

    def __str__(self):
        """
        重载__str__方法,相当于Java中的对象toString()方法
        :return:
        """
        print("序列({})和序列({})的最长子序列为:({})".format(self.str1, self.str2, self.str_lcs))


if __name__ == "__main__":
    a = "xyxxzxyzxy"
    b = "zxzyyzxxyxxz"
    my_lcs = LCS(a, b)  # 构造一个具有两个字符串的对象
    lcs_chart, lcs_flag = my_lcs.get_flag()  # 获得计算列表和标记列表
    # 显示结果矩阵和标记矩阵
    print('结果矩阵如下:')
    for i in lcs_chart:
        print(i)
    print("标记矩阵如下:")
    for j in lcs_flag:
        print(j)
    # 求两个序列的最长子序列最优解并显示
    my_lcs.get_cls(lcs_flag, a.__len__(), b.__len__())
    my_lcs.__str__()

运行结果

最长公共子序列问题Python实现_第1张图片

利用标记矩阵自底向上求最长公共子序列,标记”↖“表示ai=bi此处的值为左上角的值+1,标记”←“表示ai≠bi(左边的值大于上边的值)取值为左边的值,标记”↑“表示ai≠bi(上边的值大于或等于左边的值)取值为上边的值。

最长公共子序列问题Python实现_第2张图片

最终序列(xyxxzxyzxy)和序列(zxzyyzxxyxxz)的最长公共子序列的最优解为(xyxxxz)。


其他问题

输出最长公共子序列
关于两个序列的所有最长公共子序列的解,可以通过递归的方式实现。同样为利用标记矩阵自底向上求最长公共子序列,标记”↖“表示ai=bi此处的值为左上角的值+1,标记”←“表示ai≠bi(左边的值大于上边的值)取值为左边的值,标记”↑“表示ai≠bi(上边的值大于左边的值)取值为上边的值,标记”O“表示ai≠bi(上边的值等于左边的值)取值为上边或左边的值。
下图给出部分思路(只进行部分递归),即使用自底向上的方式行进,如果为”↑“则路径向上,为“←”则路径向左,为“↖”则路径向左上,为“O”表示进行分化(此条路径上和左两条都可以行进),达到矩阵的边缘即达到路径的终点,一条路径标记为”↖”形成的字符串代表一个最长公共子序列的解。
最长公共子序列问题Python实现_第3张图片

降低空间复杂度
①我们可以将标记矩阵和结果矩阵合并为一个含数值和标记的对象矩阵优化算法空间复杂度;
②目前设计的算法空间复杂度为Θ((m+1)(n+1))。可以通过降维操作,使算法空间复杂度从Θ((m+1)(n+1))降为Θ(min{m,n}),即选取子序列长度最小的作为一维列表的长度,将从左至右逐行填表的方式转换为在一列内位至上而下填表(如下图所示,实际空间为橙色区域)。
最长公共子序列问题Python实现_第4张图片
ps: 使用第二种空间优化方法将导致无法或者很难求出最优解和最长公共子序列的所有解。


欢迎大家留言交流

你可能感兴趣的:(算法入门)