這篇日志主要為了記錄這幾天的學習成果。
最長公共子序列根據要不要求子序列連續分兩種情況。
只考慮兩個串的情況,假設兩個串長度均為n.
一,子序列不要求連續。
(1)動態規划(O(n*n))
(轉自:http://www.cnblogs.com/xudong-bupt/archive/2013/03/15/2959039.html)
動態規划采用二維數組來標識中間計算結果,避免重復的計算來提高效率。
1)最長公共子序列的長度的動態規划方程
設有字符串a[0...n],b[0...m],下面就是遞推公式。字符串a對應的是二維數組num的行,字符串b對應的是二維數組num的列。
另外,采用二維數組flag來記錄下標i和j的走向。數字"1"表示,斜向下;數字"2"表示,水平向右;數字"3"表示,豎直向下。這樣便於以后的求解最長公共子序列。
代碼:

1 #include<stdio.h> 2 #include<string.h> 3 4 char a[500],b[500]; 5 char num[501][501]; ///記錄中間結果的數組 6 char flag[501][501]; ///標記數組,用於標識下標的走向,構造出公共子序列 7 void LCS(); ///動態規划求解 8 void getLCS(); ///采用倒推方式求最長公共子序列 9 10 int main() 11 { 12 int i; 13 strcpy(a,"ABCBDAB"); 14 strcpy(b,"BDCABA"); 15 memset(num,0,sizeof(num)); 16 memset(flag,0,sizeof(flag)); 17 LCS(); 18 printf("%d\n",num[strlen(a)][strlen(b)]); 19 getLCS(); 20 return 0; 21 } 22 23 void LCS() 24 { 25 int i,j; 26 for(i=1;i<=strlen(a);i++) 27 { 28 for(j=1;j<=strlen(b);j++) 29 { 30 if(a[i-1]==b[j-1]) ///注意這里的下標是i-1與j-1 31 { 32 num[i][j]=num[i-1][j-1]+1; 33 flag[i][j]=1; ///斜向下標記 34 } 35 else if(num[i][j-1]>num[i-1][j]) 36 { 37 num[i][j]=num[i][j-1]; 38 flag[i][j]=2; ///向右標記 39 } 40 else 41 { 42 num[i][j]=num[i-1][j]; 43 flag[i][j]=3; ///向下標記 44 } 45 } 46 } 47 } 48 49 void getLCS() 50 { 51 52 char res[500]; 53 int i=strlen(a); 54 int j=strlen(b); 55 int k=0; ///用於保存結果的數組標志位 56 while(i>0 && j>0) 57 { 58 if(flag[i][j]==1) ///如果是斜向下標記 59 { 60 res[k]=a[i-1]; 61 k++; 62 i--; 63 j--; 64 } 65 else if(flag[i][j]==2) ///如果是斜向右標記 66 j--; 67 else if(flag[i][j]==3) ///如果是斜向下標記 68 i--; 69 } 70 71 for(i=k-1;i>=0;i--) 72 printf("%c",res[i]); 73 }
(2)轉化為最長遞增子序列問題,O( n*log(n) )
(轉自:http://karsbin.blog.51cto.com/1156716/966387)
注意到num[i][j]僅在A[i]==B[j]處才增加,對於不相等的地方對最終值是沒有影響的。故而枚舉相等點處可以對上述動態規划算法進行優化。
舉例說明:
A:abdba
B:dbaaba
則 1:先順序掃描A串,取其在B串的所有位置:
2:a(2,3,5) b(1,4) d(0)。
3:用每個字母的反序列替換,則最終的最長嚴格遞增子序列的長度即為解。
替換結果:532 41 0 41 532
最大長度為3.
對於一個滿足最長嚴格遞增子序列的序列,該序列必對應一個匹配的子串。
反序是為了在遞增子串中,每個字母對應的序列最多只有一個被選出。
反證法可知不存在更大的公共子串,因為如果存在,則求得的最長遞增子序列不是最長的,矛盾。
最長遞增子序列可在O(NLogN)的時間內算出。
二,子序列要求連續
(1) 暴力枚舉(O(n^4))
方法: 枚舉B串所有子串,對比確定該子串是否為A串的某一子串,返回最長子串的長度。
復雜度分析: B串子串個數為O(n^2), 確定子串是否為A 串的一部分,為O(n^2),故而總的復雜度為O(n^4)
(2) KMP優化匹配過程( O(n^3) )
在算法一中用KMP優化子串與A串的匹配過程,可以將匹配過程優化為線性時間O(n),故而總的復雜度為O(n^3).
(3) 引入KMP( O(n^2) )
方法: 將B串的所有后綴串(n個),與A串做KMP匹配,返回匹配過程中最長配對長度。 時間復雜度為O(n^2)
(4) 后綴數組解法(O(n*log(n)) )
(轉自:https://www.byvoid.com/blog/lcs-suffix-array)
關於后綴數組的構建方法以及Height數組的性質,本文不再具體介紹,可以參閱IOI國家集訓隊2004年論文《后綴數組》(許智磊)和IOI國家集訓隊2009年論文《后綴數組——處理字符串的有力工具》(羅穗騫)。后綴數組可以在線性時間建立起來,DC3.
回顧一下后綴數組,SA[i]表示排名第i的后綴的位置,Height[i]表示后綴SA[i]和SA[i-1]的最長公共前綴(Longest Common Prefix,LCP),簡記為Height[i]=LCP(SA[i],SA[i-1])。連續的一段后綴SA[i..j]的最長公共前綴,就是H[i-1..j]的最小值,即LCP(SA[i..j])=Min(H[i-1..j])。
求N個串的最長公共子串,可以轉化為求一些后綴的最長公共前綴的最大值,這些后綴應分屬於N個串。具體方法如下:
設N個串分別為S1,S2,S3,...,SN,首先建立一個串S,把這N個串用不同的分隔符連接起來。S=S1[P1]S2[P2]S3...SN-1[PN-1]SN,P1,P2,...PN-1應為不同的N-1個不在字符集中的字符,作為分隔符(后面會解釋為什么)。
接下來,求出字符串S的后綴數組和Height數組,可以用倍增算法,或DC3算法。
然后二分枚舉答案A,假設N個串可以有長度為A的公共字串,並對A的可行性進行驗證。如果驗證A可行,A'(A'<A)也一定可行,嘗試增大A,反之嘗試縮小A。最終可以取得A的最大可行值,就是這N個串的最長公共子串的長度。可以證明,嘗試次數是O(logL)的。
於是問題就集中到了,如何驗證給定的長度A是否為可行解。方法是,找出在Height數組中找出連續的一段Height[i..j],使得i<=k<=j均滿足Height[k]>=A,並且i-1<=k<=j中,SA[k]分屬於原有N個串S1..SN。如果能找到這樣的一段,那么A就是可行解,否則A不是可行解。
具體查找i..j時,可以先從前到后枚舉i的位置,如果發現Height[i]>=A,則開始從i向后枚舉j的位置,直到找到了Height[j+1]<A,判斷[i..j]這個區間內SA是否分屬於S1..SN。如果滿足,則A為可行解,然后直接返回,否則令i=j+1繼續向后枚舉。S中每個字符被訪問了O(1)次,S的長度為NL+N-1,所以驗證的時間復雜度為O(NL)。
到這里,我們就可以理解為什么分隔符P1..PN-1必須是不同的N-1個不在字符集中的字符了,因為這樣才能保證S的后綴的公共前綴不會跨出一個原有串的范圍。
后綴數組是一種處理字符串的強大的數據結構,配合LCP函數與Height數組的性質,后綴數組更是如虎添翼。利用后綴數組,容易地求出了多個串的LCS,而且時空復雜度也相當優秀了。雖然比起后綴樹的解法有所不如,但其簡明的思路和容易編程的特點卻在實際的應用中並不輸於后綴樹。
(4) 后綴數(O(n) )
將A#B$作為字符串壓入后綴樹,找到最深的非葉節點,且該節點的葉節點既有#也有$(無#)。由於后綴樹可以在線性時間建立,而且遍歷后綴樹需要線性時間(該后綴樹中節點數目不大於 2(|A|+|B|)),故而總的時間為線性。