題目鏈接:http://poj.org/problem?id=1458
題目大意:給出兩個字符串,求出這樣的一個最長的公共子序列的長度:子序列中的每個字符都能在兩個原串中找到,而且每個字符的先后順序和原串中的先后順序一致。
輸入有若干行,每行是兩個字符串。對每一行輸入的兩個字符串,輸出最長公共子串的長度。
Sample Input
abcfbc abfcab
programming contest
abcd mnp
Sample Output
4
2
0
算法分析
參考1:北大郭煒老師mooc課程
參考2:http://blog.csdn.net/u013480600/article/details/40741333
參考3:http://blog.csdn.net/lz161530245/article/details/76943991
輸入兩個串s1,s2,
設MaxLen(i,j)表示:s1的左邊i個字符形成的子串,與s2左邊的j個字符形成的子串的最長公共子序列的長度(i,j從0開始算)
MaxLen(i,j) 就是本題的“狀態”
假定 len1 = strlen(s1),len2 = strlen(s2)
那么題目就是要求 MaxLen(len1,len2)
顯然:
MaxLen(n,0) = 0 ( n= 0…len1)
MaxLen(0,n) = 0 ( n=0…len2)
遞推公式:
if(s1[i-1] == s2[j-1]) //s1的最左邊字符是s1[0]
MaxLen(i,j) = MaxLen(i-1,j-1) + 1;
else
MaxLen(i,j) = Max(MaxLen(i,j-1),MaxLen(i-1,j) );
時間復雜度O(mn),其中m,n是兩個字串長度。
關於證明,可以閱讀參考2和參考3的證明過程。大概過程記錄如下:
我們用Ax表示序列A的連續前x項構成的子序列,即Ax= a1,a2,……ax, By= b1,b2,……by, 我們用LCS(x, y)表示它們的最長公共子序列長度,那原問題等價於求LCS(m,n)。為了方便我們用L(x, y)表示Ax和By的一個最長公共子序列。 讓我們來看看如何求LCS(x, y)。我們令x表示子序列,考慮最后一項 第(1)種情況:Ax = By 那么它們L(Ax, By)的最后一項一定是這個元素! 為什么呢?為了方便,我們令t=Ax=By, 我們用反證法:假設L(x,y)最后一項不是t, 則要么L(x,y)為空序列(別忘了這個),要么L(x,y)的最后一項是Aa=Bb ≠ t, 且顯然有a<x,b<y。無論是哪種情況我們都可以把t接到這個L(x,y)后面,從而得到一個更長的公共子序列。矛盾! 如果我們從序列Ax中刪掉最后一項ax得到Ax-1,從序列By中也刪掉最后一項by得到By-1,(多說一句角標為0時,認為子序列是空序列),則我們從L(x,y)也刪掉最后一項t得到的序列是L(x – 1, y - 1)。為什么呢?和上面的道理相同,如果得到的序列不是L(x - 1, y - 1),則它一定比L(x - 1, y - 1)短,那么它后面接上元素t得到的子序列L(x,y)也比L(x - 1, y - 1)接上元素t得到的子序列短,這與L(x, y)是最長公共子序列矛盾。 因此L(x,y)=L(x-1,y-1)最后接上元素t,也就是說: LCS(Ax, By) = LCS(x - 1, y - 1) + 1 第(2)種情況:Ax ≠ By 仍然設t=L(Ax,By)的最后一個字符,或者L(Ax,By)是空序列(這時t是未定義值不等於任何值)。 則t≠Ax和t≠By至少有一個成立,因為t不能同時等於兩個不同的值嘛! (2.1) 如果t≠Ax,則有L(x,y)=L(x-1,y),因為根本沒Ax的事嘛。 也就是說:LCS(x,y) = LCS(x – 1, y) (2.2) 如果t≠By,同理有L(x,y)= L(x,y-1)。 也就是說:LCS(x,y) = LCS(x, y – 1) 可是,我們事先並不知道t,由定義,我們取最大的一個,因此這種情況下,有LCS(x,y)=max(LCS(x–1,y),LCS(x,y–1))。 看看目前我們已經得到了什么結論: LCS(x,y) = (1) LCS(x - 1,y - 1) + 1 如果Ax = By (2) max(LCS(x – 1, y) , LCS(x, y – 1)) 如果Ax ≠ By 這是一個顯然的遞推式,光有遞推可不行,初值是什么呢? 顯然,一個空序列和任何序列的最長公共子序列都是空序列!所以我們有: LCS(x,y) = (1) LCS(x - 1,y - 1) + 1 如果Ax = By (2) max(LCS(x – 1, y) , LCS(x, y – 1)) 如果Ax ≠ By (3) 0 如果x=0或者y=0 到此我們求出了計算最長公共子序列長度的遞推公式。我們實際上計算了一個(n + 1)行(m + 1)列的表格(行是0..n,列是0..m),也就這個二維度數組LCS(n,m)。
1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 char sz1[5005]; 5 char sz2[5005]; 6 int maxLen[5005][5005]; 7 int main() 8 { 9 while( cin >> sz1 >> sz2 ) 10 { 11 int length1 = strlen( sz1); 12 int length2 = strlen( sz2); 13 int nTmp; 14 int i,j; 15 for( i = 0;i <= length1; i ++ ) maxLen[i][0] = 0; 16 for( j = 0;j <= length2; j ++ ) maxLen[0][j] = 0; 17 for( i = 1;i <= length1;i ++ ) 18 { 19 for( j = 1; j <= length2; j ++ ) 20 { 21 if( sz1[i-1] == sz2[j-1] ) 22 maxLen[i][j] = maxLen[i-1][j-1] + 1; 23 else 24 maxLen[i][j] = max(maxLen[i][j-1],maxLen[i-1][j]); 25 } 26 } 27 cout << maxLen[length1][length2] << endl; 28 } 29 return 0; 30 }
上面的題目並沒有要求輸出最長的公共子序列。假如要輸出最長公共子序列,可以閱讀參考3的代碼:(也可以暫時跳過,本文末尾有代碼實現。)
1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 int LCSLength(char* str1, char* str2, int **b) 5 { 6 int i,j,length1,length2,len; 7 length1 = strlen(str1); 8 length2 = strlen(str2); 9 10 //雙指針的方法申請動態二維數組 11 int **c = new int*[length1+1]; //共有length1+1行 12 for(i = 0; i < length1+1; i++) 13 c[i] = new int[length2+1];//共有length2+1列 14 15 for(i = 0; i < length1+1; i++) 16 c[i][0]=0; //第0列都初始化為0 17 for(j = 0; j < length2+1; j++) 18 c[0][j]=0; //第0行都初始化為0 19 20 for(i = 1; i < length1+1; i++) 21 { 22 for(j = 1; j < length2+1; j++) 23 { 24 if(str1[i-1]==str2[j-1])//由於c[][]的0行0列沒有使用,c[][]的第i行元素對應str1的第i-1個元素 25 { 26 c[i][j]=c[i-1][j-1]+1; 27 b[i][j]=0; //輸出公共子串時的搜索方向 28 } 29 else if(c[i-1][j]>c[i][j-1]) 30 { 31 c[i][j]=c[i-1][j]; 32 b[i][j]=1; 33 } 34 else 35 { 36 c[i][j]=c[i][j-1]; 37 b[i][j]=-1; 38 } 39 } 40 } 41 /* 42 for(i= 0; i < length1+1; i++) 43 { 44 for(j = 0; j < length2+1; j++) 45 printf("%d ",c[i][j]); 46 printf("\n"); 47 } 48 */ 49 len=c[length1][length2]; 50 for(i = 0; i < length1+1; i++) //釋放動態申請的二維數組 51 delete[] c[i]; 52 delete[] c; 53 return len; 54 } 55 void PrintLCS(int **b, char *str1, int i, int j) 56 { 57 if(i==0 || j==0) 58 return ; 59 if(b[i][j]==0) 60 { 61 PrintLCS(b, str1, i-1, j-1);//從后面開始遞歸,所以要先遞歸到子串的前面,然后從前往后開始輸出子串 62 printf("%c",str1[i-1]);//c[][]的第i行元素對應str1的第i-1個元素 63 } 64 else if(b[i][j]==1) 65 PrintLCS(b, str1, i-1, j); 66 else 67 PrintLCS(b, str1, i, j-1); 68 } 69 70 int main(void) 71 { 72 char str1[100],str2[100]; 73 int i,length1,length2,len; 74 printf("請輸入第一個字符串:"); 75 gets(str1); 76 printf("請輸入第二個字符串:"); 77 gets(str2); 78 length1 = strlen(str1); 79 length2 = strlen(str2); 80 //雙指針的方法申請動態二維數組 81 int **b = new int*[length1+1]; 82 for(i= 0; i < length1+1; i++) 83 b[i] = new int[length2+1]; 84 len=LCSLength(str1,str2,b); 85 printf("最長公共子序列的長度為:%d\n",len); 86 printf("最長公共子序列為:"); 87 PrintLCS(b,str1,length1,length2); 88 printf("\n"); 89 for(i = 0; i < length1+1; i++)//釋放動態申請的二維數組 90 delete[] b[i]; 91 delete[] b; 92 system("pause"); 93 return 0; 94 }
查找並輸出最長公共子序列也可以參考https://wenku.baidu.com/view/7e96c94f2b160b4e767fcfc9.html
空間上的優化:
觀察上面算法中的關鍵代碼:
for( i = 1;i <= length1;i ++ ) { for( j = 1; j <= length2; j ++ ) { if( sz1[i-1] == sz2[j-1] ) maxLen[i][j] = maxLen[i-1][j-1] + 1; else maxLen[i][j] = max(maxLen[i][j-1],maxLen[i-1][j]); } }
可以發現,計算maxLen數組第i行時用到的只有第i行與第i-1行。我們的目的是要計算maxLen[length1][length2],所以,可以考慮只保存兩行即可,也就是使用滾動數組只保存兩行。
代碼如下:(參考來源)
cur表示當前需要求的那一行的下標。
1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 char sz1[5005]; 5 char sz2[5005]; 6 int maxLen[2][5005]; 7 int main() 8 { 9 int i,j,length1,length2,cur=0; 10 11 while( cin >> sz1 >> sz2 ) 12 { 13 length1 = strlen( sz1); 14 length2 = strlen( sz2); 15 for( i=0;i<2; i++ ) maxLen[i][0]=0; 16 for( j=0;j<=length2;j++ ) maxLen[0][j]=0; 17 cur=0; 18 19 for( i = 1;i <= length1;i ++ ) 20 { 21 cur ^= 1; 22 for( j = 1; j <= length2; j ++ ) 23 { 24 if( sz1[i-1] == sz2[j-1] ) 25 maxLen[cur][j] = maxLen[cur^1][j-1] + 1; 26 else 27 maxLen[cur][j] = max(maxLen[cur][j-1],maxLen[cur^1][j]); 28 } 29 } 30 cout << maxLen[cur][length2] << endl; 31 } 32 return 0; 33 }
下面修改一下代碼尋找出一個最長公共子序列。
上面經過空間優化后,也只是尋找到了最長公共子序列的長度,那么如何得到一個最長公共子序列而僅僅不是簡單的長度呢?其實我們離真正的答案只有一步之遙!


參考上圖,我們建立一個二維數組ans[][],在尋找最長公共子序列的長度時用ans[i][j]記錄LCS(i,j)是如何來的(從左邊、上邊或是從左上),ans[i][j]等於1,2,3分別表示:
L(x,y) = L(x, y – 1)
L(x,y)= L(x – 1, y)
L(x,y) = L(x,- 1 y- 1)末尾接上Ax
當ans[i][j]等於3時字符串1的第i個字符(或字符串2的第j個字符,其實兩者相同)肯定是最長公共子序列的一部分,要保留到temp[ ]中。所以從ans[][]右下角逆推即可求出temp[ ],然后逆序輸出temp[]即可。代碼如下:
1 //51Nod動態規划教程例題 求最長公共子序列的長度並輸出一個最長公共子序列 2 #include <iostream> 3 #include <cstring> 4 using namespace std; 5 #define maxN 5005 6 char sz1[maxN]; 7 char sz2[maxN]; 8 int maxLen[2][maxN]; 9 char ans[maxN][maxN]={0}; 10 11 void printLCS(int len1,int len2);//輸出一個最長公共子序列 12 int main() 13 { 14 int i,j,length1,length2,cur=0; 15 freopen("poj1458.in","r",stdin); 16 while( cin >> sz1 >> sz2 ) 17 { 18 memset(ans,0,sizeof(char)*maxN*maxN); 19 length1 = strlen( sz1); 20 length2 = strlen( sz2); 21 for( i=0;i<2; i++ ) maxLen[i][0]=0; 22 for( j=0;j<=length2;j++ ) maxLen[0][j]=0; 23 cur=0; 24 25 for( i = 1;i <= length1;i ++ ) 26 { 27 cur ^= 1; 28 for( j = 1; j <= length2; j ++ ) 29 { 30 if( sz1[i-1] == sz2[j-1] ) 31 { 32 maxLen[cur][j] = maxLen[cur^1][j-1] + 1; 33 ans[i][j]=3; 34 } 35 else 36 { 37 //maxLen[cur][j] = max(maxLen[cur][j-1],maxLen[cur^1][j]); 38 if(maxLen[cur][j-1]>maxLen[cur^1][j]) 39 { 40 maxLen[cur][j]=maxLen[cur][j-1]; 41 ans[i][j]=1; 42 } 43 else 44 { 45 maxLen[cur][j]=maxLen[cur^1][j]; 46 ans[i][j]=2; 47 } 48 } 49 } 50 } 51 cout << maxLen[cur][length2] << endl; 52 if(maxLen[cur][length2]>0) printLCS(length1,length2); 53 } 54 return 0; 55 } 56 void printLCS(int len1,int len2)//輸出一個最長公共子序列 57 { 58 char temp[maxN]; 59 int i=len1,j=len2,k=0; 60 while(ans[i][j]!=0) 61 { 62 if(ans[i][j]==3) { temp[k++]=sz1[i-1]; i--;j--; } 63 else if(ans[i][j]==1) 64 { 65 j--; 66 } 67 else if(ans[i][j]==2) 68 { 69 i--; 70 } 71 } 72 for(k--;k>=0;k--) printf("%c",temp[k]); 73 printf("\n"); 74 }
