最长公共子序列(LCS)长度的快速(呃)算法

时间:2011-07-02 08:09:52

标签: algorithm optimization edit-distance lcs

问题:需要两个字符串之间的LCS长度。字符串的大小最多为100个字符。字母表是通常的DNA,4个字符“ACGT”。动态方法不够快。

我的问题是,我正在处理很多对(我认为数百万的等级)。我相信我已经将LCS_length函数的调用减少到最小可能,因此使程序运行得更快的唯一方法是使用更高效的LCS_Length函数。

我已经开始实施通常的动态编程方法。 这给出了正确答案,希望能够正确实施。

#define arrayLengthMacro(a) strlen(a) + 1
#define MAX_STRING 101

static int MaxLength(int lengthA, int lengthB);

/* 
 * Then the two strings are compared following a dynamic computing
 * LCS table algorithm. Since we only require the length of the LCS 
 * we can get this rather easily.
 */
int LCS_Length(char *a, char *b)
{
    int lengthA = arrayLengthMacro(a),lengthB = arrayLengthMacro(b), 
        LCS = 0, i, j, maxLength, board[MAX_STRING][MAX_STRING];

        maxLength = MaxLength(lengthA, lengthB);

    //printf("%d %d\n", lengthA, lengthB);
    for (i = 0; i < maxLength - 1; i++)
    {
        board[i][0] = 0;
        board[0][i] = 0;
    }

    for (i = 1; i < lengthA; i++)
    {
        for (j = 1; j < lengthB; j++)
        {
/* If a match is found we allocate the number in (i-1, j-1) incremented  
 * by 1 to the (i, j) position
 */
            if (a[i - 1] == b[j - 1])
            {

                board[i][j] = board[i-1][j-1] + 1;
                if(LCS < board[i][j])
                {
                    LCS++;
                }
            }
            else
            {
                if (board[i-1][j] > board[i][j-1])
                {
                    board[i][j] = board[i-1][j];
                }
                else
                {
                    board[i][j] = board[i][j-1];
                }
            }
        }
    }

    return LCS;
}

那应该是O(mn)(希望如此)。

然后在搜索速度时我找到了这篇文章Longest Common Subsequence 迈尔斯给了O(ND) paper。我试过这个,它将LCS与最短的编辑脚本(SES)联系起来。 它们给出的关系是D = M + N-2L,其中D是SES的长度,M和N是两个串的长度,L是LCS长度。但是在我的实现中并不总是正确的。我给出了我的实现(我认为是正确的):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define arrayLengthMacro(a) strlen(a)

int LCS_Length (char *a, char *b);
int MinLength (int A, int B);
int Max (int A, int B);
int snake(int k, int max, char *a, char *b, int lengthA, int lengthB);

int main(void)
{
    int L;
    char a[] = "tomato", b[] = "potato"; //must give LCS = 4
    L =  LCS_Length(a, b);
    printf("LCS: %d\n", L );    
    char c[] = "GTCGTTCGGAATGCCGTTGCTCTGTAAA", d[] = "ACCGGTCGAGTGCGCGGAAGCCGGCCGAA"; // must give LCS = 20
    L =  LCS_Length(c, d);
    printf("LCS: %d\n", L );
    char e[] = "human", f[] = "chimpanzee"; // must give LCS = 4
    L =  LCS_Length(e, f);
    printf("LCS: %d\n", L );
    char g[] = "howareyou", h[] = "whoareyou"; // LCS =8
    L =  LCS_Length(g, h);
    printf("LCS: %d\n", L );
    char i[] = "TTCTTTCGGTAACGCCTACTTTATGAAGAGGTTACATTGCAATCGGGTAAATTAACCAACAAGTAATGGTAGTTCCTAGTAGAGAAACCCTCCCGCTCAC", 
        j[] = "GCACGCGCCTGTTGCTACGCTCTGTCCATCCTTTGTGTGCCGGGTACTCAGACCGGTAACTCGAGTTGCTATCGCGGTTATCAGGATCATTTACGGACTC"; // 61
    L =  LCS_Length(i, j);
    printf("LCS: %d\n", L );


    return 0;
}

int LCS_Length(char *a, char *b)
{

    int D, lengthA = arrayLengthMacro(a), lengthB = arrayLengthMacro(b), 
        max, *V_, *V, i, k, x, y;

    max = lengthA + lengthB;
    V_ = malloc(sizeof(int) * (max+1));
    if(V_ == NULL)
    {
        fprintf(stderr, "Failed to allocate memory for LCS");
        exit(1);
    }
    V = V_ + lengthA;
    V[1] = 0;

    for (D = 0; D < max; D++)
    {
        for (k = -D; k <= D; k = k + 2)
        {
            if ((k == -D && V[k-1] < V[k+1]) || (k != D && V[k-1] < V[k+1]))
            {
                x = V[k+1];
            }
            else
            {
                x = V[k-1] + 1;
            }
            y = x - k;
            while ((x < lengthB) && (y < lengthA) && (a[x+1] == b[y+1]))
            {
                x++;
                y++;
            }
            V[k] = x;
            if ((x >= lengthB) && (y >= lengthA))
            {
                return (lengthA + lengthB - D)/2;
            }
        }
    }
    return  (lengthA + lengthB - D)/2;
}

主要有一些例子。 例如。 “番茄”和“土豆”(不评论),LCS长度为4。 实现发现SES(代码中的D)是2个而不是4个我想要的(删除“t”,插入“p”,删除“m”,插入“t”)。我倾向于认为O(ND)算法也可能计算替换,但我不确定。

任何可实现的方法(我没有很强的编程技能)都是受欢迎的。 (如果有人知道如何利用小字母表)。

编辑:我认为最重要的是,在任何时候我都会在任意两个字符串之间使用LCS函数。所以它不仅仅是字符串s1,而是其他几百万字符串。 s1000可能是s200,s100是s000,s100000则是250。也不太可能跟踪大多数使用过的字符串。 我要求LCS长度不是近似值,因为我实现了近似算法,我不想添加额外的错误。

编辑:刚跑完callgrind。对于10000个字符串的输入,对于不同的字符串对,我似乎将lcs函数称为50,000,000次。 (10000个字符串是我想要运行的最低值,最大值是100万(如果可行的话))。

3 个答案:

答案 0 :(得分:1)

有几种方法可以让您的计算更快:

  • 您可以使用A *搜索(通过使用启发式,部分对齐到(i,j)必须具有| i-j |删除或插入),而不是简单的动态编程。
  • 如果您将一个序列与一大堆其他序列进行比较,则可以通过计算导致该前缀的部分的动态编程表(或A *搜索状态)来节省时间并重新使用该部分你的计算。如果您坚持使用动态编程表,您可以按字典顺序对字符串库进行排序,然后只丢弃更改的部分(例如,如果您有'香蕉',并希望将其与'巴拿马'和'泛美主义'进行比较,你可以重复使用覆盖'panam'的表格部分。
  • 如果大多数字符串非常相似,您可以通过查找公共前缀并从计算中排除公共前缀来节省时间(例如,“panama”的LCS和“panamericanism”是公共前缀“panam”加上LCS的“a”和“ericanism”)
  • 如果字符串非常不同,您可以使用符号计数来获得编辑数量的下限(例如,“AAAB”到“ABBB”需要至少2次编辑,因为在一个且仅有一个中有3个As 1 A在另一个)。这也可以用于A *搜索的启发式算法。

编辑:对于比较相同的蜇伤情况,一个人建议使用BK-Tree数据结构 Efficient way of calculating likeness scores of strings when sample size is large?

答案 1 :(得分:1)

我不熟悉动态编程算法 计算LCS,但我想指出一些事情:

首先,如果你比较非常大,那么O(ND)方法才有意义 相似的字串。对你来说情况似乎并非如此。

其次,加快LCD_Length函数的渐近性能是 可能不是你应该关注的,因为你的弦很漂亮 短。如果你只关心找到相似或不相似的对(而不是全部 对'确切的LCS),然后Yannick提到的BK树看起来很有希望 要走的路。

最后,有些事情让我对你的DP实施感到困扰。正确的 在你的代码中解释“board [i] [j]”是“最长的子序列” 字符串a [1..i],b [1..j]“(我在这种表示法中使用了1索引)。因此, 你的for循环应该包括i = lengthA和j = lengthB。它看起来像你 通过引入arrayLengthMacro(a)来破解代码中的这个bug,但是那样 hack在算法的上下文中没有意义。一旦“板”填满, 你可以在board [lengthA] [lengthB]中查找解决方案,这意味着你可以得到 摆脱不必要的“if(LCS&lt; board [i] [j])”阻止并返回 板[lengthA] [LENGTHB]。此外,循环边界看起来错了 初始化(我很确定它应该是(i = 0; i&lt; = maxLength; i ++) 其中maxLength = max(lengthA,lengthB))。

答案 2 :(得分:0)

我建议抓住Gusfield Algorithms on Strings, Trees and Sequences的副本,这是关于计算生物学的字符串操作。