這篇日誌主要爲了記錄這幾天的學習成果。html
最長公共子序列根據要不要求子序列連續分兩種狀況。算法
只考慮兩個串的狀況,假設兩個串長度均爲n.編程
一,子序列不要求連續。數組
(1)動態規劃(O(n*n))數據結構
(轉自:http://www.cnblogs.com/xudong-bupt/archive/2013/03/15/2959039.html)ide
動態規劃採用二維數組來標識中間計算結果,避免重複的計算來提升效率。函數
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|)),故而總的時間爲線性。