【动态规划】LCS算法 python

问题描述1
求两字符串的连续最大公共子字符串(The Longest Common Substring)
这个LCS问题就是求两个字符串最长公共子串的问题。解法就是用一个矩阵来记录两个字符串中所有位置的两个字符之间的匹配情况,若是匹配则为1,否则为0。然后求出对角线最长的1的序列,其对应的位置就是最长匹配子串的位置。如图1所示,在对角线上,连续的1就代表了两字符串对应的位置连续相等。


image.png

从该矩阵中找到最长的全为1的对角线,就可以找到最大公共子串,分别找出对应的字符即可求出这个最长子串。如图1,有两个最长的公共子串,bab与aba。

综上所述,基本思路分为两步:

算一个匹配矩阵存放两个字符串每个字符之间的匹配情况,相同为1,不同为0。
求这个矩阵中连续的最长的值为1的对角线,得到这个对角线之后我们就得到了公共子串的长度和位置(行列索引值)
改进算法
求出字符串匹配矩阵是很简单的事情,但是当我们得到0/1矩阵时,我们要怎么去找到最长的对角线呢?这是一个比较麻烦的问题。

我们可以对这个算法进行改进,即在填充这个矩阵的值的时候,不仅仅是0和1,而是把公共子串的最大长度的值填入相应的位置。我们的目的很清楚,就是在图1的基础上把最长的对角线的值改为1.2.3…,代表了公共子串的长度,看下面两个图会比较清楚:


image.png

image.png

我们想一下,把矩阵变成这样之后,每个元素的值m[i][j]代表的是子串s1的前i部分和子串s2的前j部分中最大公共子串的长度。那么其实这个矩阵中最大的元素的值就代表了公共子串的最大长度,而且通过这个最大元素的行列索引我们就可以定位到这个公共子串在原字符串中的结束位置,再通过长度就可以反推出公共子串的内容了。下面我们讲一下代码思路:

思路:当两个字符串中找到匹配的字符之后,s1[i]=s2[j],那么我们要填入矩阵[i,j]位置的值取决于它的左上方的值,填入的值为左上方的值+1,代表这个字符可以加入现有的公共子串。

代码

def find_lcsubstr(s1, s2):
    m = [[0 for i in range(len(s2) + 1)] for j in range(len(s1) + 1)]  # 生成0矩阵,为方便后续计算,比字符串长度多了一列
    mmax = 0  # 最长匹配的长度
    p = 0  # 最长匹配对应在s1中的最后一位
    for i in range(len(s1)):
        for j in range(len(s2)):
            if s1[i] == s2[j]: # 如果相等,则加入现有的公共子串
                m[i + 1][j + 1] = m[i][j] + 1
                if m[i + 1][j + 1] > mmax:
                    mmax = m[i + 1][j + 1]
                    p = i + 1
    return s1[p - mmax:p], mmax  # 返回最长子串及其长度

问题描述2
求两个字符串的最大公共子序列(LCS)(序列可以不连续)
子串要求字符必须是连续的,但是子序列就不是这样。最长公共子序列是一个十分实用的问题,它可以描述两段文字之间的“相似度”,即它们的雷同程度,从而能够用来辨别抄袭。对一段文字进行修改之后,计算改动前后文字的最长公共子序列,将除此子序列外的部分提取出来,这种方法判断修改的部分,往往十分准确。

例如:alibaba和ababila的最大公共子序列为ababa

算法思路
思路:还是使用一个匹配矩阵来存放相关信息。此时矩阵的元素代表字符串s1前i个字符与字符串s2前j个字符的最大公共子序列长度。那么如何填充这个矩阵呢?
对于字符串a和字符串b,长度分别为m,n,先考虑他们的最后一个字符。

如果相等,说明这个字符一定可以成为最大公共子序列的最后一个字符,那么我们就可以不管这个字符,去求a[0:m-1] 和 b[0:n-1]的最大公共子序列。
对于矩阵来说,这个关系就是:如果s1[i] = s2[j],那么m[i][j] = m[i-1][j-1] + 1
图中就表示为斜对角左上方的值加1
如果不等,那么就变成去求a[0:m-1]和b[0:n](丢弃a的最后一位) 或者a[0:m]和 b[0:n-1](丢弃b的最后一位)的最大公共子序列
对于矩阵来说,关系是:m[i][j] = MAX{m[i-1][j], m[i][j-1]}
图中就表示为是上方的值和左方的值中取大的那个
例如:图2中红色的2:判断行列字符都是b,因此为左上角1+1得到2;图2中绿色的3:判断行列字符b和a不等,取上方和左方的最大值3.


image.png

整个矩阵求出之后,右下角的元素值就是字符串a和b的最大公共子序列的长度了。

根据上文的推到思路可以找到回溯思路,从右下角开始
(1)如果当前元素等于其左上角加1,那么当前元素行列相等,是公共子序列中的一个,保存下来。
(2)如果当前元素来自上方或左方,当前元素不保存。

按照此思路,从5开始,形成如图所示的回溯路线,只有斜向上的箭头对应的行元素纳入最大公共子序列,求得最大公共子序列为ababa。


image.png

代码
一个矩阵记录两个字符串中匹配情况,若是匹配则为左上方的值加1,否则为左方和上方的最大值。一个矩阵记录转移方向,然后根据转移方向,回溯找到最长子序列。

def find_lcseque(s1, s2):
    # 生成字符串长度加1的0矩阵,m用来保存对应位置匹配的结果
    m = [[0 for x in range(len(s2) + 1)] for y in range(len(s1) + 1)]
    # d用来记录转移方向
    d = [[None for x in range(len(s2) + 1)] for y in range(len(s1) + 1)]

    for p1 in range(len(s1)):
        for p2 in range(len(s2)):
            if s1[p1] == s2[p2]:  # 字符匹配成功,则该位置的值为左上方的值加1
                m[p1 + 1][p2 + 1] = m[p1][p2] + 1
                d[p1 + 1][p2 + 1] = 'ok'
            elif m[p1 + 1][p2] > m[p1][p2 + 1]:  # 左值大于上值,则该位置的值为左值,并标记回溯时的方向
                m[p1 + 1][p2 + 1] = m[p1 + 1][p2]
                d[p1 + 1][p2 + 1] = 'left'
            else:  # 上值大于左值,则该位置的值为上值,并标记方向up
                m[p1 + 1][p2 + 1] = m[p1][p2 + 1]
                d[p1 + 1][p2 + 1] = 'up'
    (p1, p2) = (len(s1), len(s2))
    print(numpy.array(d))
    s = []
    while m[p1][p2]:  # 不为None时
        c = d[p1][p2]
        if c == 'ok':  # 匹配成功,插入该字符,并向左上角找下一个
            s.append(s1[p1 - 1])
            p1 -= 1
            p2 -= 1
        if c == 'left':  # 根据标记,向左找下一个
            p2 -= 1
        if c == 'up':  # 根据标记,向上找下一个
            p1 -= 1
    s.reverse()
    return ''.join(s)

if __name__ == '__main__':
    print(find_lcseque( 'alibaba','ababila'))

对这个回溯矩阵比划一下可以发现跟上面画的图一模一样


image.png

最后附上参考的博客并表示感谢:
https://blog.csdn.net/wateryouyo/article/details/50917812
https://blog.csdn.net/yebanxin/article/details/52190683
https://blog.csdn.net/yebanxin/article/details/52186706
————————————————
版权声明:本文为CSDN博主「斯科菲尔德666」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Scofield971031/article/details/89027314

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容