LCS(longest common sequence)算法的实现(十分详细)

一、问题描述
有两个字符串,求二者的最长公共子序列。
最长公共子序列:不必连续但必须有序的子列(与子串区分,子串是连续的)

二:解决方法
第一种方法:穷举法 ,就是一个一个的对比,但这个方法的时间复杂度为O(2^n),故而不多做赘述。
第二种方法:分而治之+动态规划法。

三:动态规划
关于动态规划法我的理解是增加空间代价来减少时间代价,这个方法常用于寻找最优解。
分而治之类似,通过某种手段将一个大问题分解成为若干个小问题,再分解成更小的问题,但这样的话会导致重复计算,就是小问题与小问题之间有相同的地方,从而导致时间代价大大提高,而动态规划法通过将每个小问题的解求出来,放到一个表中,要用的时候拿出来,这样来减少运算量,减少时间代价,提高空间代价,但相比于超高的时间代价而言这是个更优解。
四:问题分析
我们用分而治之的方法来对这个问题进行分析。有如下字符串:

Xi<x1,x2,x3,…,xn>
Yi<y1,y2,y3,…,ym>

1,若xn=ym
这种情况下,最长子序列的最后一个字符一定是xn(ym),所以我们可以将同时去掉这两个字符,之后再将改动后的字符串的最长子序列求出来加上这个字符即可。
2,若xn!=ym
这种情况下我们可以分为两种情况,一种是去掉Xi的最后一个字符,另一个是去掉Yi的最后一个字符,分别对这两个改动后的字符串求最长子序列。
总体来说如下:

在这里插入图片描述

从这两种情况来看我们需要用到递归的方法,一次一次的将问题拆解,这也就是分而治之,同时这也暴露的一个问题,也就是我上面所提到的,重复计算,时间代价大大提高,故而在此基础上我们要结合动态规划的方法。
既然我们要将大问题分为小问题,那么为什么不先把小问题的解提前算好呢?
为了先算好这个问题,我们得逆向考虑。不是从尾巴开始一刀一刀砍,而是从头开始一个一个算。这里有两个字符串:

在这里插入图片描述

利用这个表我们可以得知每个子问题的解。
那么这个表是怎么来的呢?
我们暂且忽略第一排0和第一列0,这个是用代码实现的时候用到的,也可以理解是两个空字符串。
我们分析第二排,单拎出这个A,和上面这个字符串一个一个对比,A和B存在零个相同的字符,故表格中的数为0,往后移,A和BD有0个相同的字符,故为0,往后移,A和BDC有0个,往后移,A和BDCA有一个相同的字符,故为1,以此推类,A分析完分析AB,以此推类。最终得到这样一个表格。
但这样还不够,我们注意到表中的箭头号,其中左上箭头表示连个字符串的最后一个字符相同,故同时去掉这个箭头就指向对角线,而左箭头和上箭头则指向二者不同从而分为两个子问题,两个子问题中的更大解,这个就联系到了我们上面分而治之所分析的。
综上我们将这个表制作了出来,有子问题的解,也有方向,从而我们就不需要用递归来找了最优解了,而是通过这个表来找个最优解,从这个表中我们找到一条直通0的通路,将这条通路的每个左上箭头所代表的字符连接成串就是我们要找的最长公共子序列,到这里我们的分析结束了,代码方面也可以轻松的写出来。
五、代码实现
代码方面我们只需要做出一个表就行,在这里我用到的是结构体数组,结构体中包含了解和方向。

#include <iostream>
#include <string>
using namespace std;
struct attribute
{
    int derection;
    int sum;
};
int maxoftwo(int a1, int a2)
{
    if (a1 > a2)
        return a1;
    return a2;
}
void LCS(string s1, string s2, int lth1, int lth2)
{
    attribute att[lth1 + 1][lth2 + 1];//加一是因为还有一排0和一列0,这里读者可以结合代码想一想为什么
    for (int i = 0; i < lth1 + 1; i++)
    {
        for (int j = 0; j < lth2 + 1; j++)
        {
            att[i][j].derection = 1;
            att[i][j].sum = 0;
        }
    }
    for (int i = 0; i < lth1; i++)
    {
        for (int j = 0; j < lth2; j++)
        {
            if (s1[i] == s2[j])
            {
                att[i + 1][j + 1].sum = att[i][j].sum + 1;
                att[i + 1][j + 1].derection = 0;
            }
            if (s1[i] != s2[j])
            {
                att[i + 1][j + 1].sum = maxoftwo(att[i][j + 1].sum, att[i + 1][j].sum);
                att[i + 1][j + 1].derection = att[i][j + 1].sum > att[i + 1][j].sum ? -1 : 1;
            }
        }
    }
    char s[att[lth1][lth2].sum];
    int k = att[lth1][lth2].sum - 1;//最长子序列数组
    int i = lth1, j = lth2;
    while (att[i][j].sum != 0)
    {
        if (att[i][j].derection == 0)
        {
            s[k] = s1[i - 1];
            i--;
            j--;
            k--;
        }
        if (att[i][j].derection == 1)
            j--;
        if (att[i][j].derection == -1)
            i--;
    }
    for (int i = 0; i < att[lth1][lth2].sum; i++)
        cout << s[i];
    cout << endl;
}
int main()
{
    string s1, s2;
    cin >> s1 >> s2;
    string s;
    int length1 = s1.length();
    int length2 = s2.length();
    LCS(s1, s2, length1, length2);
}

至此,这个问题我们就解决了,关于直接用递归的方法我也不太想去写了,太麻烦了,效率又低,算了算了,读者若有兴趣可以自己写写,可以的话可以私信发给我康康。
注:本篇文章的图片来自于:
程序员编程艺术第十一章:最长公共子序列(LCS)问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值