组合问题的DP妙用
本文将以几个例子来讲解。一般我们在使用dp时都是先证明最优性原理,本文的话就跳过这一部分了,因为这个证明一般比较简单
1.最长子序列问题
问题描述:给定一个序列,要求找出其中最长的子序列的长度;
例子:
输入:5 2 8 6 3 6 9 7
输出:4
解释:最长子序列为2 3 6 9或者2 3 6 7
分析:
我们这样来定义子问题
设L(i)表示从a1到ai的最长子序列(且该序列的最后一个元素为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];
}
}
}
int index=0;
for(int i=1;i<a.length;i++){
if(L[i]>index){
index=i;
}
}
//打印结果
for(int i=0;i<L[index];i++){
System.out.print(x[index][i]+" ");
}
System.out.println();
System.out.println("最长子序列长度为:"+L[index]);
}
}
时间复杂度: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.背包问题
这个问题在我的背包九讲专题有解答