组合问题的DP妙用

组合问题的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.背包问题

这个问题在我的背包九讲专题有解答

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值