本文從三個層次分析最大公共子序列
- 最大公共子序列長度
- 最大公共子序列
- 算法分析
首先來個區別:單詞"cnblogs"
- 子序列:從單詞中抽取字符,不能保證連續抽取。如”cn"、“cns"、”bgs"
- 連續子序列:從單詞中連續抽取字符。如“bolog"、”cnbl"
最長公共子序列(LCS:Longest Common Subsequence)顧名思義,就是幾個詞語中最長的相同子序列。比如“cnblogs"和”belong"最大公共子序列是“blog"
最長公共子序列是個非常有用的算法,可以判斷兩段文字間的”雷同程度“,從而可以判別抄襲。下面先介紹幾種找出最長公共子序列長度的算法:
最大公共子序列長度
1.暴力算法
對於含有n個字符一個句子,每個位置有兩種可能(出現 or 不出現),因此總共有2*2*2....總共2^n-1個(排除空序列)序列。這樣找出來知道,在和另一個句子中的子序列意義比較(為了少算點可以只比角長度相同的)。
顯然,這種方法也太暴力了,指數增長,一點技術含量沒有。直接舍去了。
2.遞歸算法
把一個大問題看成幾個已經解決了的子問題的綜合。
兩個字符串,分別是stra和strb。如果對應長度是lena和lenb。那么就是求解LCS(lena, lenb)。此時先比較stra[lena-1]和strb[lenb-1](字符串是從0開始計數的)。
- 如果相同則等於LCS(lena-1,lenb-1)+1,此時LCS(lena-1,lenb-1)不知道,接着遞歸
- 如果不同則比較LCS(lena-2,lenb-1)和LCS(lena-1,lenb-2),前者大,就等於前者;后者大,后者。中間步驟不知道,接着遞歸
- 如果遞歸到了LCS()中的一個數為-1了那就相當於存在空串了,公共的長度肯定是0了。
參考程序:
#include <stdio.h> #include <string.h> int LCS(int m, int n); char a[100]; char b[100]; int main() { strcpy(a, "cnblogs"); strcpy(b, "belong"); int lena = strlen(a); int lenb = strlen(b); printf("LCS:%d\n", LCS(lena-1, lenb-1)); return 0; } int LCS(int m, int n) { if(m==-1 || n==-1) return 0; else if(a[m] == b[n]) return 1 + LCS(m-1, n-1); else return LCS(m-1, n) > LCS(m, n-1) ? LCS(m-1, n):LCS(m, n-1); }
3.動態規划
和遞歸算法的大化小問題思路不同,動態規划是把一個問題轉化成一些列的單階段問題。
在利用動態規划找出最長公共子序列時,目標是求LCR(lena,lenb),我們把任意兩點的LCR求出來,此時要用二位數組表示。
基本原理公式還是那樣:
此時注意,字符串計數是從0開始的,現在用二維數組表示,就不能像上面一樣出現-1了,現在用二維數組表示個數時,從1開始,即LCR[m][n],表示stra[m-1]和strb[n-1]之間的最大子序列長度。
現在用具體的例子闡明動態規划的過程:
stra = "cnblogs"
strb = "belong"
- LCR[m][0]=0(表示:str[m-1] 和”空“間的關系);同理LCR[0][n]=0
- LCR[1][1]:先看stra[0]和strb[0]間想不相同('c'和‘b'不相同),就比較LCR[1][0] 和LCR[1][0]都為0,那么LCR[1][1]為0;
- 一直這樣做下去......
參考程序:
#include <stdio.h> #include <string.h> char stra[100], strb[100]; int lena, lenb; int matrix[100][100]; void LCS(); int main() { strcpy(stra, "cnblogs"); strcpy(strb, "belong"); lena = strlen(stra); lenb = strlen(strb); memset(matrix, 0, sizeof(matrix)); LCS(); return 0; } void LCS() { int i=0, j=0; for(i=0; i<lena; i++) { for(j=0; j<lenb; j++) { if(stra[i] == strb[j]) { matrix[i+1][j+1] = matrix[i][j] + 1; } else { if(matrix[i+1][j] >= matrix[i][j+1]) { matrix[i+1][j+1] = matrix[i+1][j]; } else { matrix[i+1][j+1] = matrix[i][j+1]; } } } } printf("LCS:%d\n", matrix[lena][lenb]); }
最大公共子序列
有了最長公共子序列長度核心公式,求個長度還是很容易的,現在要求出具體的最大公共子序列。暴力算法是理論上是可以求出來的,但是過於繁瑣與低效,棄了。動態規划與遞歸思路是一樣的。
動態規划
這樣標記:
- 當stra[i] == strb[j]時,標斜向上的箭頭(記值為0)
- 當LCR[i+1][j]≥LCR[i][j+1]時,標向左箭頭(記值為1)
- 當LCR[i+1][j]<LCR[i][j+1]時,標向上箭頭(記值為-1)
尋找子序列:
- 見0記下, i--, j--
- 見1左拐,j--
- 見-1上拐,i--
圖示說明:
參考算法:
#include <stdio.h> #include <string.h> char stra[100], strb[100]; int lena, lenb; int matrix[100][100]; int tag[100][100]; void LCS(); void getLCS(); int main() { strcpy(stra, "cnblogs"); strcpy(strb, "belong"); lena = strlen(stra); lenb = strlen(strb); memset(matrix, 0, sizeof(matrix)); LCS(); getLCS(); return 0; } void LCS() { int i=0, j=0; for(i=0; i<lena; i++) { for(j=0; j<lenb; j++) { if(stra[i] == strb[j]) { matrix[i+1][j+1] = matrix[i][j] + 1; tag[i+1][j+1] = 0; } else { if(matrix[i+1][j] >= matrix[i][j+1]) { matrix[i+1][j+1] = matrix[i+1][j]; tag[i+1][j+1] = 1; } else { matrix[i+1][j+1] = matrix[i][j+1]; tag[i+1][j+1] = -1; } } } } //輸出次數矩陣 for (i=1; i<=lena; i++) { for (j=1; j<=lenb; j++) printf("%d ", matrix[i][j]); printf("\n"); } printf("****************\n"); //輸出方向轉移矩陣 for (i=1; i<=lena; i++) { for (j=1; j<=lenb; j++) printf("%d ", tag[i][j]); printf("\n"); } printf("LCS:%d\n", matrix[lena][lenb]); } void getLCS() { int i = lena, j = lenb, sum=0; char seq[100]; while(i != 0 && j != 0) { if(tag[i][j] == 0) { seq[sum] = stra[i-1]; i--; j--; sum++; } else if(tag[i][j] == 1) j--; else i--; } for(i=sum-1; i>=0; i--) printf("%c ", seq[i]); }
遞歸算法
遞歸算法輸出矩陣的思路與動態規划思路完全一致,就是在遞歸過程中標記,再回溯即可。
參考代碼:
#include <stdio.h> #include <string.h> int LCS(int m, int n); void getLCS(); char stra[100], strb[100]; int lena, lenb; int tag[100][100]; char seq[100]; int main() { int i, j; memset(tag, 0, sizeof(tag)); strcpy(stra, "cnblogs"); strcpy(strb, "belong"); lena = strlen(stra); lenb = strlen(strb); printf("LCS:%d\n", LCS(lena-1, lenb-1)); getLCS(); for(i=0; i<=lena; i++) { for(j=0; j<=lenb; j++) printf("%d ", tag[i][j]); printf("\n"); } return 0; } int LCS(int m, int n) { if(m==-1 || n==-1) { return 0; } else if(stra[m] == strb[n]) { tag[m+1][n+1] = 1; return 1 + LCS(m-1, n-1); } else { if(LCS(m, n-1) > LCS(m-1, n)) { tag[m+1][n+1] = 2; return LCS(m, n-1); } else { tag[m+1][n+1] = 3; return LCS(m-1, n); } } } void getLCS() { int i = lena, j = lenb, sum=0; while(i != 0 && j != 0) { if(tag[i][j] == 1) { seq[sum] = stra[i-1]; i--; j--; sum++; } else if(tag[i][j] == 2) j--; else i--; } printf("The lCS is:"); for(i=sum-1; i>=0; i--) printf("%c ", seq[i]); printf("\n"); }
算法分析
m表示第一個字串長度,n表示第二個字串長度。
動態規划
時間復雜度:
- 建立矩陣需要,需要花費時間o(mn)
- 回溯需要至多花費時間o(m+n)
綜上,兩者相加,時間復雜度為o(mn)
空間復雜度:
- 構建矩陣需要空間o(mn)
- 構建標記矩陣需要空間o(mn)
綜上,二者相加,空間復雜度為o(mn)