摘自 https://www.cnblogs.com/hapjin/p/5572483.html
這位大佬寫的對理解DP也很有幫助,我就直接摘抄過來了,代碼部分來自我做過的題
一,問題描述
給定兩個字符串,求解這兩個字符串的最長公共子序列(Longest Common Sequence)。比如字符串1:BDCABA;字符串2:ABCBDAB
則這兩個字符串的最長公共子序列長度為4,最長公共子序列是:BCBA
二,算法求解
這是一個動態規划的題目。對於可用動態規划求解的問題,一般有兩個特征:①最優子結構;②重疊子問題
①最優子結構
設 X=(x1,x2,.....xn) 和 Y={y1,y2,.....ym} 是兩個序列,將 X 和 Y 的最長公共子序列記為LCS(X,Y)
找出LCS(X,Y)就是一個最優化問題。因為,我們需要找到X 和 Y中最長的那個公共子序列。而要找X 和 Y的LCS,首先考慮X的最后一個元素和Y的最后一個元素。
1)如果 xn=ym,即X的最后一個元素與Y的最后一個元素相同,這說明該元素一定位於公共子序列中。因此,現在只需要找:LCS(Xn-1,Ym-1)
LCS(Xn-1,Ym-1)就是原問題的一個子問題。為什么叫子問題?因為它的規模比原問題小。(小一個元素也是小嘛....)
為什么是最優的子問題?因為我們要找的是Xn-1 和 Ym-1 的最長公共子序列啊。。。最長的!!!換句話說,就是最優的那個。(這里的最優就是最長的意思)
2)如果xn != ym,這下要麻煩一點,因為它產生了兩個子問題:LCS(Xn-1,Ym) 和 LCS(Xn,Ym-1)
因為序列X 和 序列Y 的最后一個元素不相等嘛,那說明最后一個元素不可能是最長公共子序列中的元素嘛。(都不相等了,怎么公共嘛)。
LCS(Xn-1,Ym)表示:最長公共序列可以在(x1,x2,....x(n-1)) 和 (y1,y2,...yn)中找。
LCS(Xn,Ym-1)表示:最長公共序列可以在(x1,x2,....xn) 和 (y1,y2,...y(n-1))中找。
求解上面兩個子問題,得到的公共子序列誰最長,那誰就是 LCS(X,Y)。用數學表示就是:
LCS=max{LCS(Xn-1,Ym),LCS(Xn,Ym-1)}
由於條件 1) 和 2) 考慮到了所有可能的情況。因此,我們成功地把原問題 轉化 成了 三個規模更小的子問題。
②重疊子問題
重疊子問題是啥?就是說原問題 轉化 成子問題后, 子問題中有相同的問題。咦?我怎么沒有發現上面的三個子問題中有相同的啊????
OK,來看看,原問題是:LCS(X,Y)。子問題有 ❶LCS(Xn-1,Ym-1) ❷LCS(Xn-1,Ym) ❸LCS(Xn,Ym-1)
初一看,這三個子問題是不重疊的。可本質上它們是重疊的,因為它們只重疊了一大部分。舉例:
第二個子問題:LCS(Xn-1,Ym) 就包含了:問題❶LCS(Xn-1,Ym-1),為什么?
因為,當Xn-1 和 Ym 的最后一個元素不相同時,我們又需要將LCS(Xn-1,Ym)進行分解:分解成:LCS(Xn-1,Ym-1) 和 LCS(Xn-2,Ym)
也就是說:在子問題的繼續分解中,有些問題是重疊的。
由於像LCS這樣的問題,它具有重疊子問題的性質,因此:用遞歸來求解就太不划算了。因為采用遞歸,它重復地求解了子問題啊。而且注意哦,所有子問題加起來的個數 可是指數級的哦。。。。
這篇文章中就演示了一個遞歸求解重疊子問題的示例。
那么問題來了,你說用遞歸求解,有指數級個子問題,故時間復雜度是指數級。這指數級個子問題,難道用了動態規划,就變成多項式時間了??
呵呵噠。。。。
關鍵是采用動態規划時,並不需要去一 一 計算那些重疊了的子問題。或者說:用了動態規划之后,有些子問題 是通過 “查表“ 直接得到的,而不是重新又計算一遍得到的。廢話少說:舉個例子吧!比如求Fib數列。關於Fib數列,可參考:
求fib(5),分解成了兩個子問題:fib(4) 和 fib(3),求解fib(4) 和 fib(3)時,又分解了一系列的小問題....
從圖中可以看出:根的左右子樹:fib(4) 和 fib(3)下,是有很多重疊的!!!比如,對於 fib(2),它就一共出現了三次。如果用遞歸來求解,fib(2)就會被計算三次,而用DP(Dynamic Programming)動態規划,則fib(2)只會計算一次,其他兩次則是通過”查表“直接求得。而且,更關鍵的是:查找求得該問題的解之后,就不需要再繼續去分解該問題了。而對於遞歸,是不斷地將問題分解,直到分解為 基准問題(fib(1) 或者 fib(0))
說了這么多,還是要寫下最長公共子序列的遞歸式才完整。借用網友的一張圖吧:)
c[i,j]表示:(x1,x2....xi) 和 (y1,y2...yj) 的最長公共子序列的長度。(是長度哦,就是一個整數嘛)。公式的具體解釋可參考《算法導論》動態規划章節
這張DP表很是重要,從中我們可以窺見最長公共子序列的來源,同時可以根據這張表打印出最長公共子序列的構成路徑
三,最長公共子序列模板
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N = 1000; char a[N],b[N]; int dp[N][N]; int main() { int lena,lenb,i,j; while(scanf("%s%s",a,b)!=EOF) { memset(dp,0,sizeof(dp)); lena=strlen(a); lenb=strlen(b); for(i=1;i<=lena;i++) { for(j=1;j<=lenb;j++) { if(a[i-1]==b[j-1]) { dp[i][j]=dp[i-1][j-1]+1; } else { dp[i][j]=max(dp[i-1][j],dp[i][j-1]); } } } printf("%d\n",dp[lena][lenb]); } return 0; }
最長公共子序列打印路徑的模板
遞歸法:
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N = 1010; char x[N],y[N]; int dp[N][N]; int b[N][N]; void Print(int i,int j) { if(i==0||j==0)///遞歸終止條件 { return ; } if(b[i][j]==1) { Print(i-1,j-1); printf("%c",x[i-1]); } else if(b[i][j]==2) { Print(i-1,j); } else if(b[i][j]==3) { Print(i,j-1); } } int main() { int lena,lenb,i,j; while(scanf("%s%s",x,y)!=EOF) { memset(dp,0,sizeof(dp)); memset(b,0,sizeof(b)); lena=strlen(x); lenb=strlen(y); for(i=1;i<=lena;i++) { for(j=1;j<=lenb;j++) { if(x[i-1]==y[j-1]) { dp[i][j]=dp[i-1][j-1]+1; b[i][j]=1;///來自於左上方 } else { if(dp[i-1][j]>dp[i][j-1]) { dp[i][j]=dp[i-1][j]; b[i][j]=2;///來自於左方 } else { dp[i][j]=dp[i][j-1]; b[i][j]=3;///來自於上方 } } } } Print(lena,lenb); } return 0; }
非遞歸,在這里因為是逆序的回溯,所以我使用了棧來存儲路徑
1 #include<stdio.h> 2 #include<string.h> 3 #include<stack> 4 #include<algorithm> 5 using namespace std; 6 #define N 1010 7 int dp[N][N]; 8 char c; 9 int main() 10 { 11 char a[N]; 12 char b[N]; 13 scanf("%s%s",a,b); 14 int la=strlen(a); 15 int lb=strlen(b); 16 memset(dp,0,sizeof(dp)); 17 for(int i=1; i<=la; i++) 18 { 19 for(int j=1; j<=lb; j++) 20 { 21 if(a[i-1]==b[j-1]) 22 dp[i][j]=dp[i-1][j-1]+1; 23 else 24 dp[i][j]=max(dp[i-1][j],dp[i][j-1]); 25 } 26 } 27 int i=la,j=lb; 28 stack<char>s; 29 while(dp[i][j]) 30 { 31 if(dp[i][j]==dp[i-1][j])///來自於左方向 32 { 33 i--; 34 } 35 else if(dp[i][j]==dp[i][j-1])///來自於上方向 36 { 37 j--; 38 } 39 else if(dp[i][j]>dp[i-1][j-1])///來自於左上方向 40 { 41 i--; 42 j--; 43 s.push(a[i]); 44 } 45 } 46 while(!s.empty()) 47 { 48 c=s.top(); 49 printf("%c",c); 50 s.pop(); 51 } 52 return 0; 53 }