• 对LCS算法及其变种的初步研究


    LCS的全称为Longest Common Subsequence,用于查找两个字符串中的最大公共子序列,这里需要注意区分子序列与子串,所谓子序列,指的是从前到后,可以跳跃元素筛选,而字串则必须连续筛选。

    例如AB##!C!@#E和AB123CC321E两个字符串,如果找最长公共字串,只能是AB;如果是找最长公共子列,则是ABCE。

    还有一种变种的LCS,允许元素重复,这样找到的子列将会是ABCCE,但是这样回溯是比较麻烦的,一般只能得到序··列的长度。

    下面我们先介绍基本LCS的算法,然后介绍其变体。

    【基本LCS】

    1.首先作如下约定

    ①设字符串a、b的索引从1开始,a的全长为xm,b的全长为yn。

    ②c[i][j]记录了a串1~i范围和b串1~j范围内的最长子串长度。

    2.递推式

    要求c[i][j],我们需要考虑a[i]和b[j]的关系。

    ①a[i]=b[j]:说明当前子列的末尾是a、b所共有,各退一步,就得到了上一次求得的公共子列长度,也就是c[i-1][j-1],显然两个序列仅相差了一个字符,因此c[i][j] = c[i-1][j-1]+1。

    ②a[i]≠b[j]:说明当前子列的长度在a或者b向后推进一个字符后并未变化,因为这个字符不公共,应该考虑去掉一个字符后的公共子列中较长的,也就是c[i][j] = max{c[i-1][j], c[i][j-1]}。

    这样,我们就得到了完整的递推式,下面要解决的就是递推起点的参数。

    不难发现,c[0][.]和c[.][0]都应该是0,这就是递推的起点。

    3.编程实现

    从c[1][1]一直处理到c[xm][yn]即可,需要注意的是字符索引从0开始,因此我们需要在c的索引基础上减一。

    代码如下:

    void LCS(string a, string b){
        int xm = a.length();
        int yn = b.length();
        vector<vector<int> > c(xm + 1);
        for(int x = 0; x <= xm; x++){
            c[x].resize(yn + 1);
        }
        for(int x = 1; x <= xm; x++) c[x][0] = 0;
        for(int y = 1; y <= yn; y++) c[0][y] = 0;
        for(int x = 1; x <= xm; x++)
            for(int y = 1; y <= yn; y++){
                if(a[x-1] == b[y-1]){
                    c[x][y] = c[x-1][y-1] + 1;
                }else if(c[x][y-1] >= c[x-1][y]){
                    c[x][y] = c[x][y-1];
                }else{
                    c[x][y] = c[x-1][y];
                }
            }
        printf("LCS Length:%d
    ",c[xm][yn]);
    }
    这样仅仅能得到序列的长度,如果要得到子列,需要回溯,从a和b的最后一个字符开始,根据字符关系查表c来确定是否是子列中的元素。因为这样得到的是倒序,因此需要每次插入到字符串的头部。

        string res = "";
        int i = xm, j = yn;
        while(i >= 1 && j >= 1){
            if(a[i-1] == b[j-1]){
                res.insert(res.begin(),a[i-1]);
                i--;
                j--;
            }else if(c[i][j-1] >= c[i-1][j]) j--;
            else i--;
        }
        cout << res << endl;


    【变种LCS】

    如上文所述,有时候需要考虑元素重复的情况,例如PAT上的一道题1045. Favorite Color Stripe (30)就要求计算元素可重复的最长子列,为了达到这个目的,只需要对算法稍加修改,无论什么情况,均取c[i-1][j]、c[i][j-1]和c[i-1][j-1]中的最大值,并且如果发现a、b当前字串的末尾相同,则在最大值基础上+1,这样就可以重复记录了。

    这样做的原因是,原来每次碰到相同的都去找去掉后的+1,这样即使碰到多次重复也不会造成累加,因为我们只考虑了对角线。而如果是在左、对角、上三个方向寻找最大的,则会不断累加。把两个字符串看成一张表,横着为串b,竖着为串a,假设此时比较的是a中的'x',而b中有多个'x',如果是普通LCS,对于每个‘x'都会去找对角线,这样不会造成累加,而变种LCS会找到左边已经累加过的再累加,因此就允许了重复计数。

    这样处理的问题在于无法通过回溯得到序列,而只能拿到长度。

    void LCS_changed(string a, string b){
        int xm = a.length();
        int yn = b.length();
        vector<vector<int> > c(xm + 1);
        for(int x = 0; x <= xm; x++){
            c[x].resize(yn + 1);
        }
        for(int x = 1; x <= xm; x++) c[x][0] = 0;
        for(int y = 1; y <= yn; y++) c[0][y] = 0;
        for(int x = 1; x <= xm; x++)
            for(int y = 1; y <= yn; y++){
                int max = c[x-1][y-1];
                if(c[x][y-1] > max) max = c[x][y-1];
                if(c[x-1][y] > max) max = c[x-1][y];
                if(a[x-1] == b[y-1]){
                    c[x][y] = max + 1;
                }else{
                    c[x][y] = max;
                }
            }
        printf("LCS Length:%d
    ",c[xm][yn]);
    }

  • 相关阅读:
    过河问题 贪心
    喷水装置2 贪心
    喷水装置 贪心算法
    大红数星星 图论 XD网络赛
    Bi-shoe and Phi-shoe 欧拉函数 素数
    c++ 打飞机游戏开发日志
    POJ 1129 Channel Allocation DFS 回溯
    POJ 2676 Sudoku
    LibreOJ #100. 矩阵乘法
    BZOJ 1009: [HNOI2008]GT考试
  • 原文地址:https://www.cnblogs.com/aiwz/p/6154032.html
Copyright © 2020-2023  润新知