【问题标题】:How to optimise the O(m.n) solution for longest common subsequence?如何优化最长公共子序列的 O(m.n) 解决方案?
【发布时间】:2014-07-23 02:21:35
【问题描述】:

给定两个长度为 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 个用户解决方案。有没有更聪明的技巧来降低这个问题的时间复杂度?

【问题讨论】:

  • 请正确格式化您的代码以使其可读。
  • @KonradRudolph 我应该添加更多空格吗?我是一个新程序员,所以对格式不太了解。
  • @swapedoc 每个嵌套块都应该有自己的嵌套级别,这是一个普遍的共识。此外,中缀运算符通常应该始终被单个空格包围。逻辑上独立的代码块应该用一个空行分开。这是人们同意的最低限度。其余的主要是口味问题。
  • @Brian 该链接无法从我的位置访问,我有假期,所以我在家,无法使用机构登录。您能否详细说明它的要点或给我发送其他链接阅读论文?
  • @Brian 这是收费的。 at ResearchGate 提供预览版,但他们不提供下载(至少非会员不提供)。

标签: c++ optimization dynamic-programming


【解决方案1】:

LCS 是一个经典的、经过深入研究的计算机科学问题,对于有两个序列的情况,已知其下限为 O(n·m)。

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

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

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

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

【讨论】:

  • @larsmans 这需要预处理。综合起来会慢一些。此外,您提供的链接是针对“最长公共 子字符串”的,这是一个类似但非常不同的问题。
  • @KonradRudolph 后缀树方法比这更好吗?
  • 问题是:我对测试用例一无所知 两个字符串都包含不超过 50000 个小写拉丁字母。使用后缀树最初看起来是一个不错的选择,但我认为只有在我有超过 2 个字符串要检查,否则下限是相同的。我想知道这 14 种解决方案是如何被接受的
  • 后缀数组和后缀树方法是针对最长公共子字符串(不是子序列)。
  • @Brain 是的,但它们可以适应。这通常在生物信息学中完成(您几乎从不对子字符串匹配感兴趣,它总是与子序列有关)。
【解决方案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

【讨论】:

    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-08-24
      • 1970-01-01
      • 2014-11-08
      • 1970-01-01
      • 2012-09-19
      • 2012-12-12
      • 2011-03-01
      相关资源
      最近更新 更多