2011-07-02 113 views
10

問題:需要兩個字符串之間的LCS的長度。字符串的大小最多爲100個字符。字母表是通常的DNA,4個字符「ACGT」。動態方法不夠快。最長公共子序列長度(LCS)的快速(er)算法

我的問題是,我正在處理很多對和很多對(我可以看到數以億計的等級)。我相信我已經將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. 實現發現它是5個SES(代碼中的D)出現2,而不是我期望的4 (刪除「t」,插入「p」,刪除「m」,插入「t」)。我傾向於認爲也許O(ND)算法也會計算替換,但我不確定這一點。

任何可以實施的方法(我沒有很多編程技能)都是值得歡迎的。 (如果有人知道如何利用小字母例如)。

編輯:我認爲這將是有益的說,在任何其他的頂部,我在任何時候使用任何兩個字符串之間的LCS函數。所以它不僅是字符串說s1,相比之下有幾百萬人。它可能是s200與s1000,然後s0與s10000,然後250與s100000。不太可能跟蹤最常用的字符串。 我需要的LCS長度不是一個近似值,因爲我正在實現一個近似算法,我不想添加額外的錯誤。

編輯:剛剛跑過callgrind。對於10000個字符串的輸入,我似乎將不同的字符串對稱爲lcs函數大約50,000,000次。 (10000字符串是我想運行的最低字符數,最大值是100萬字符(如果可行的話))。

+0

在第二個版本中,你並沒有刪除'V_',如果你調用這個函數數億次,肯定會導致問題。每次調用函數時,都應該避免分配內存,因爲這會減慢你的速度。在你的情況下,我只是在棧上聲明一個固定長度的數組'int V_ [2 * MAX_STRING + 2]'。 – TonyK

+0

LCS使用的空間是O(mn)。你可以把它放到O(n),它可以讓你的程序更快,因爲緩存的命中率會增加。但是,如果n,m〜10^3我通常使用這個。您可以嘗試我之前寫過的[this](http://www.ideone.com/4rG7k)代碼,並及時查看其中的差異(如果有的話)。 –

+2

@logic_max:這是對算法的一個很好的修改,但第19行的副本將撤消避免緩存命中的任何性能優勢(無論如何,我認爲這對於〜40kb陣列來說並不太可怕)。您可以通過保留兩個int指針來避免該副本,即在每次迭代中交換的L和L_old。 –

回答

1

有幾種方法可以讓你的計算速度更快:

  • 不是純動態規劃,您可以使用A *搜索(使用啓發式這部分對準高達(I,J)必然有|在其中刪除或插入)。
  • 如果您正在將一個序列與其他序列進行比較,則可以通過計算引導至該前綴的部分的動態編程表(或A *搜索狀態)並重新使用該部分來節省時間的計算。如果你堅持使用動態編程表,你可以按字典順序對字符串庫進行排序,然後只丟棄發生變化的部分(例如,如果你有'香蕉',並想與'巴拿馬'和'泛美主義'進行比較,你可以重新使用表中涵蓋'panam'的部分)。
  • 如果大多數字符串非常相似,可以通過查找通用前綴並從計算中排除常用前綴來節省時間(例如,「巴拿馬」和「泛美主義」的LCS是普通前綴「panam」加「a」和「ericanism」的LCS)
  • 如果字符串非常不相似,則可以使用符號計數來獲得編輯次數的下限(例如,「AAAB」至「ABBB」至少需要2編輯,因爲有3個在另一箇中只有1個)。這也可以用於A *搜索的啓發式。

編輯:爲比較對的,同設置-刺的情況下,一個人建議在 Efficient way of calculating likeness scores of strings when sample size is large?

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,但是在算法的上下文中這個 黑客沒有意義。一旦「board」被填充,你可以在board [lengthA] [lengthB]中查找解決方案,這意味着你可以得到 擺脫不必要的「if」塊並返回 板[長度A] [長度B]。此外,循環邊界在初始化時看起來不對(我很確定它應該是for(i = 0; i < = maxLength; i ++) 其中maxLength = max(lengthA,lengthB))。

+0

@ Julian Panetta:謝謝你所有的觀點。事實上,O(ND)不是一個很好的選擇(它更像是我想的一個試驗)。 第二點,我需要確切的LCS長度。 第三點,感謝您在代碼中提到的內容。我有一段路要走,直到我獲得編程邏輯的經驗和便利。 – Yiannis

+0

噢,好吧,既然你需要所有對的LCS長度,我想你應該在比較一個字符串A和所有其他字符串B時嘗試Yannick重複使用DP表的建議。構建這個的一個好方法是構建一個trie字典併爲每個A運行一個DFS。然後,每次你下降trie時(從B讀取一封信),你填寫一行表。每次你回溯你減少你的行索引。每次你在單詞樹中找到一個單詞節點時,就已經爲(A,B)計算了一個LCS。這相當於對字符串進行排序,但使用更簡潔的代碼。注意:我現在使用行索引的B索引。 –

+0

我對存儲東西有點猶豫,因爲整個過程已經耗費空間。例如,當我有一百萬個字符串時,該內存需要多少內存? – Yiannis