如何针对最长公共子序列优化O(m.n)解?

时间:2014-06-02 21:18:10

标签: c++ optimization dynamic-programming

给定两个长度为x1的字符串X和长度为y1的字符串Y,找到两个字符串中从左到右(但不一定在连续块中)出现的最长字符序列。

  

例如,如果X = ABCBDAB且Y = BDCABA,则LCS(X,Y)= {“BCBA”,“BDAB”,“BCAB”}和LCSlength为4.

我使用标准解决方案来解决这个问题:

if(X[i]=Y[j]) :1+LCS(i+1,j+1)
if(X[i]!=Y[j]) :LCS(i,j+1) or LCS(i+1,j), whichever is greater

然后我使用了记忆,使其成为标准的DP问题。

    #include<iostream>
    #include<string>
    using namespace std;
    int LCS[1024][1024];

     int LCSlen(string &x, int x1, string &y, int y1){

        for(int i = 0; i <= x1; i++)
            LCS[i][y1] = 0;

        for(int j = 0; j <= y1; j++)
             LCS[x1][j] = 0;

        for(int i = x1 - 1; i >= 0; i--){

            for(int j = y1 - 1; j >= 0; j--){

                LCS[i][j] = LCS[i+1][j+1];

                if(x[i] == y[j])
                LCS[i][j]++;

                if(LCS[i][j+1] > LCS[i][j])
                LCS[i][j] = LCS[i][j+1];

                if(LCS[i+1][j] > LCS[i][j])
                LCS[i][j] = LCS[i+1][j];

            }
        }

    return LCS[0][0];
    } 

    int main()
    {
        string x;
        string y;
        cin >> x >> y;
        int x1 = x.length() , y1 = y.length();
        int ans = LCSlen( x, x1, y, y1);
        cout << ans << endl;
        return 0;
    }

运行here,这是我在SPOJ中使用的解决方案,我超出了时间限制和/或运行时错误。

尚未接受14个用户解决方案。有没有更智能的技巧来减少这个问题的时间复杂性?

3 个答案:

答案 0 :(得分:4)

LCS是一个经典的,经过充分研究的计算机科学问题,对于具有两个序列的情况,已知它的下界是O(n·m)。

此外,您的算法实现没有明显的效率错误,因此它应该尽可能快地运行(尽管使用动态大小的2D矩阵而不是超大的2D矩阵可能是有益的,它占用4 MiB的内存,并且需要经常缓存失效(这是一项代价高昂的操作,因为它会导致从主内存到处理器缓存的转移,这比缓存内存访问慢几个数量级。)

就算法而言,为了降低理论界限,您需要利用输入结构的细节:例如,如果您反复搜索其中一个字符串,则可能需要构建一个搜索索引来进行一些处理时间,但会使实际搜索更快。其中两个经典变体是suffix arraysuffix tree

如果已知至少有一个字符串非常短(<64个字符),则可以使用Myers’ bit vector algorithm,其执行速度要快得多。不幸的是,算法实现起来远非微不足道。存在an implementation in the SeqAn library,但使用库本身有一个陡峭的学习曲线。

(有趣的是,该算法在生物信息学中经常应用,并已在人类基因组计划的序列组装过程中使用。)

答案 1 :(得分:2)

虽然由于超出时间限制我仍然没有获得AC,但我能够实现线性空间算法。如果有人想看,这里是Hirschbirg算法的c ++实现。

#include <cstdlib>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <string>
#include <cstdio>
using namespace std;

int* compute_help_table(const string & A,const string & B);
string lcs(const string & A, const string & B);
string simple_solution(const string & A, const string & B);

int main(void) {
    string A,B;
    cin>>A>>B;

    cout << lcs(A, B).size() << endl;

    return 0;
}

string lcs(const string &A, const string &B) {
    int m = A.size();
    int n = B.size();

    if (m == 0 || n == 0) {
        return "";
    }
    else if(m == 1) {
        return simple_solution(A, B);
    }
    else if(n == 1) {
        return simple_solution(B, A);
    }
    else {
        int i = m / 2;

        string Asubstr = A.substr(i, m - i);
        //reverse(Asubstr.begin(), Asubstr.end());
        string Brev = B;
        reverse(Brev.begin(), Brev.end());

        int* L1 = compute_help_table(A.substr(0, i), B);
        int* L2 = compute_help_table(Asubstr, Brev);

        int k;
        int M = -1;
        for(int j = 0; j <= n; j++) {
            if(M < L1[j] + L2[n-j]) {
                M = L1[j] + L2[n-j];
                k = j;
            }
        }

        delete [] L1;
        delete [] L2;

        return lcs(A.substr(0, i), B.substr(0, k)) + lcs(A.substr(i, m - i), B.substr(k, n - k));
    }
}

int* compute_help_table(const string &A, const string &B) {
    int m = A.size();
    int n = B.size();

    int* first = new int[n+1];
    int* second = new int[n+1];

    for(int i = 0; i <= n; i++) {
        second[i] = 0;
    }

    for(int i = 0; i < m; i++) {
        for(int k = 0; k <= n; k++) {
            first[k] = second[k];  
        }

        for(int j = 0; j < n; j++) {
            if(j == 0) {
                if (A[i] == B[j])
                    second[1] = 1;
            }
            else {
                if(A[i] == B[j]) {
                    second[j+1] = first[j] + 1;
                }
                else {
                    second[j+1] = max(second[j], first[j+1]);
                }
            }
        }
    }

    delete [] first;
    return second;
}

string simple_solution(const string & A, const string & B) {
    int i = 0;
    for(; i < B.size(); i++) {
        if(B.at(i) == A.at(0))
            return A;
    }

    return "";
}

正在运行here

答案 2 :(得分:1)

如果两个字符串共享一个公共前缀(例如“ABCD”和“ABXY”共享“AB”),则该字符串将成为LCS的一部分。普通后缀也是如此。因此,对于某些字符串对,您可以在启动DP算法之前跳过最长公共前缀和最长公共后缀来获得一些速度;这不会改变最坏情况的界限,但它会将最佳情况复杂度改为线性时间和恒定空间。