組合問題的DP妙用
本文將以幾個例子來講解。一般我們在使用dp時都是先證明最優性原理,本文的話就跳過這一部分了,因爲這個證明一般比較簡單
1.最長子序列問題
問題描述:給定一個序列,要求找出其中最長的子序列的長度;
例子:
輸入:5 2 8 6 3 6 9 7
輸出:4
解釋:最長子序列爲2 3 6 9或者2 3 6 7
分析:
我們這樣來定義子問題
設L(i)表示從a1到ai的最長子序列的長度,那麼有
①i=1或者沒有比ai小的aj(j<i),L(i)=1
②L(i)=max {L(j)+1}(當aj<ai時)
在下面代碼中我保存了每一個子問題對應狀態的最長子序列
代碼實現:
import org.junit.Test;
public class Main {
int Max = 65535;
@Test
//x[][]數組,比如x[i]這一列就保存了L[i]對應的子序列
//算法過程如下:
/*
1.初始化L[i]=1,x[i][0]=a[i]
2.開始循環遍歷數組按照子問題的狀態方程來遞推
*/
public void Test(){
int a[]={5,2,8,6,3,6,9,7};
getLongest(a);
}
void getLongest(int []a){
int L[]=new int[a.length];
int x[][]=new int[a.length][a.length];
//初始化
for(int i=0;i<a.length;i++){
L[i]=1;
x[i][0]=a[i];
}
for(int i=1;i<a.length;i++){
for(int j=i-1;j>=0;j--){
if(a[j]<a[i]&&L[j]+1>L[i]){
L[i]=L[j]+1;
for(int k=0;k<L[j];k++)
x[i][k]=x[j][k];
x[i][L[i]-1]=a[i];
}
}
}
//打印結果
for(int i=0;i<L[a.length-1];i++){
System.out.print(x[a.length-1][i]+" ");
}
System.out.println();
System.out.println("最長子序列長度爲:"+L[a.length-1]);
}
}
時間複雜度:O(n^2)
2.最長公共子序列
問題描述:
現在是給兩個序列找出X,Y序列的最長公共子序列
子問題分析:
L(i,j)表示{x1,x2,…,xi},與{y1,y2,…,yj}的最長公共子序列長度,那麼初始化子問題應該爲
L(0,0)=L(0,j)=L(i,0)=0;
易得狀態方程:
L(i,j)
①當xi=yj時,L(i,j)=L(i-1,j-1)+1
②當xi!=yj時,L(i,j)=max{L(i,j-1),L(i-1,j)};
當然光有長度是肯定不夠的,我們還需要保留對應的序列,這裏給出的辦法是:
若X與Y序列的大小分別爲m與n;
定義一個數組狀態數組s[m+1][n+1];
①若Xi=Yj,那麼s[i][j]=1,表示他下一個搜索的是L[i-1][j-1],對應的下一個狀態爲s[i-1][j-1]
②若Xi!=Yj且L[i-1][j]>=L[i][j-1],那麼s[i][j]=2,表示下一個搜索的是L[i-1][j],對應的下一個狀態爲s[i-1][j]
③若Xi!=Yj且L[i-1][j]<L[i][j-1],那麼s[i][j]=3,表示下一個搜索的是L[i][j-1],對應的下一個狀態爲s[i][j-1]
最後我們進行填充L[i][j]時把s[i][j]也進行記錄
結束後從s[m][n]出發回溯即可(回溯時如果s[i][j]==1說明爲需要的點)
代碼:
import org.junit.Test;
public class Main {
int Max = 65535;
@Test
public void Test(){
char[] X = {' ','a','b','c','b','d','b'};
char[] Y = {' ','a','c','b','b','a','b','d','b','b'};
char[] Z = new char[X.length];
getLongest(X,Y,Z);
for(int i=0;i<Z.length;i++){
if(Z[i]!=0)
System.out.print(Z[i]+" ");
}
}
//該函數將x,y的最長公共子序列放入z中
void getLongest(char x[],char y[],char[] z){
int [][]L = new int[x.length][y.length];
int [][]S = new int[x.length][y.length];
//初始化
for(int i=0;i<x.length;i++)
L[i][0]=0;
for(int j=0;j<y.length;j++)
L[0][j]=0;
//遍歷填充L[i][j]
for(int i=1;i<x.length;i++)
for(int j=1;j<y.length;j++){
if(x[i]==y[j]){
L[i][j] = L[i-1][j-1]+1;
S[i][j] = 1;
}else if(x[i]!=y[j]){
if(L[i-1][j]>=L[i][j-1]){
L[i][j]=L[i-1][j];
S[i][j] = 2;
}else{
L[i][j]=L[i][j-1];
S[i][j] = 3;
}
}
}
//接下來回溯(從S[x.length][y.length]開始
int i=x.length-1,j=y.length-1;
int k = L[x.length-1][y.length-1]-1;
//一直到邊界退出即可
while(i>=1&&j>=1){
if(S[i][j]==1){
z[k--]=x[i];
i--;
j--;
}else if(S[i][j]==2){
i--;
}else
{
j--;
}
}
}
}
結果:
如果你比較細心會發現X={a,b,c,b,d,b},Y={a,c,b,b,a,b,d,b,b}的最長公共子序列也可以爲
a,b,b,d,b 原因在於你的條件判斷,如果在複製S[i][j]時改爲
若Xi!=Yj且L[i-1][j]>L[i][j-1],那麼s[i][j]=3
若Xi!=Yj且L[i-1][j]<=L[i][j-1],那麼s[i][j]=2
就可以得到上面結論,這裏留給讀者自己嘗試。
3.揹包問題
這個問題在我的揹包九講專題有解答