动态规划-最长的公共子序列(java)

先说一下什么是公共子序列:
例如对于字符串"ABCDEF",ABCD是其一个子序列,ABEF也是一个子序列。子序列不要求连续性,与公共子字符串区分一下,而且最长公共子序列不一定是唯一的。

问题分析:

给定两个子序列X={x1,x2,x3…,xm}和Y={y1,y2,y3…yn},找出X和Y的一个最长的公共子序列。

例如:X={A,B,C,B,A,D,B},Y={B,C,B,A,A,C},那么最长公共子序列是B,C,B,A。

如何找到最长公共子序列呢?

能不能用动态规划来解决该问题呢?

下面分析该问题是否具有最优子结构性质。

(1)分析最优解的结构特征

假设已经知道Zk={z1,z2,z3…,zk}是Xm={x1,x2,x3,…xm}和Yn={y1,y2,y3,…yn}的最长公共子序列。这个假设很重要,我们都是这样假设已经知道最优解。

那么可以分三种情况讨论。

①. xm=yn=zk;那么Zk-1={z1,z2,z3…,zk-1}是Xm-1和Yn-1的最长公共子序列,应该容易理解吧。

②. xm≠yn, xm≠zk;我们可以把xm去掉,那么Zk是Xm-1和Yn的最长公共子序列。

③. yn≠xm, yn≠zk; 我们可以把yn去掉,那么Zk是Xm和Yn-1的最长公共子序列。

(2)建立最优值的递归式

设c[i][j]表示Xi和Yj的最长公共子序列长度。

①. xm=yn=zk;那么c[i][j]=c[i-1][j-1]+1;

②. xm≠yn, xm≠zk;那么我们只需要求解Xi和Yj-1的最长公共子序列和Xi-1和Yj的最长公共子序列,比较它们的长度那个更大,就取哪一个值。即c[i][j]=max{c[i][j-1],c[i-1][j]}.

③. 最长公共子序列长度递归表达式:

Java最长公共子序列递归 java求最长公共子序列_公共子序列

(3)自底向上计算最优解,并计算最优解和最优策略
i=1时:{x1}和{y1,y2,y3,…yn}中的字符一一比较,按递归式求解并记录最长公共子序列的长度。
i=2时:{x2}和{y1,y2,y3,…yn}中的字符一一比较,按递归式求解并记录最长公共子序列的长度。

i=m时:{xm}和{{y1,y2,y3,…yn}中的字符一一比较,按递归式求解并记录最长公共子序列的长度。
(4)构造最优解
上面的求解过程只是得到了最长公共子序列长度,并不知道最长公共子序列是什么,那么怎么办呢?
例如,现在已经求出c[m][n]=5,表示Xm和Yn的最长公共子序列是5,那么这个5怎么得到的呢?我们可以反向追踪5是从哪里来的。根据递归式,有如下情况。
xi=yj时;c[i][j]=c[i-1][j-1]+1;
xi≠yj时;c[i][j]=max{c[i][j-1],c[i-1][j]};
那么c[i][j]的来源一共有三个:c[i][j]=c[i-1][j-1]+1,c[i][j]=c[i][j-1],c[i][j]=c[i-1][j],在第三步自底向上计算最优值时,用一个辅助数组b[i][j]记录这三个来源:
c[i][j]=c[i-1][j-1]+1,b[i][j]=1;
c[i][j]=c[i][j-1],b[i][j]=2;
c[i][j]=c[i-1][j],b[i][j]=3;
这样就可以根据b[m][n]反向追踪最长公共子序列,当b[i][j]=1时,输出xi;当b[i][j]=2时;追踪c[i][j-1];当b[i][j]=3时,追踪c[i-1][j];,直到i=0或j=0停止。

算法设计

最长公共子序列问题满足动态规划的最优子结构性质,可以自底向上逐步得到最优解
(1)确定合适的数据结构
采用二维数组c[][]来记录最长公共子序列的长度,二维数组b[][]来记录最长公共子序列的长度的来源,以便算法结束时倒推求解得到该最长公共子序列
(2)初始化
输入两个字符串s1,s2,初始化c[][]第一行第一列元素为0.
(3)循环阶段
①.i=1;s1[0]与s2[j-1]比较,j=1,2,3,…,len2。
如果s1[0]=s2[j-1],则c[i][j]=c[i-1][j-1]+1;并记录最优策略来源b[i][j]=1,
如果s1[0]≠s2[j-1],则公共子序列的长度来源于c[i][j-1]和c[i-1][j]的最大值,根据来源记录最优策略来源b[i][j]。
②. i=2;s1[1]与s2[j-1]比较,j=1,2,3,…,len2。
③ .以此类推,直到i>len1时,算法结束,这时c[len1][len2]就是最长公共子序列长度
(4)构造最优解
根据最优决策信息数组b[][]递归构造最优解,即输出最长公共子序列。因为我们在求最长公共子序列长度c[i][j]的过程中,用b[i][j]记录了c[i][j]的来源,那么就可以根据b[i][j]数组倒推最优解。

填表就跟着程序走一遍,也方便理解,这里就不填了

代码

import java.util.Scanner;
public class Main {
    static int N=1002;
    static int [][]c=new int[N][N];
    static int [][]b=new int[N][N];//记录最优解来源数组
    static char []s1=new char[N];
    static char []s2=new char[N];
    static int len1,len2;//s1,s2的长度
    static void LCSL(){
        int i,j;
        for(i=1;i<=len1;i++){//控制s1序列
            for(j=1;j<=len2;j++){//控制s2序列
                if(s1[i-1]==s2[j-1]) {//下标从零开始,如果当前的字符相同,则公共子序列的长度为该字符前的最长公共序列
                    c[i][j] = c[i - 1][j - 1] + 1;
                    b[i][j]=1;
                }
                else{
                    if(c[i][j-1]>=c[i-1][j]){//两者找最大值,并记录最优解来源
                        c[i][j]=c[i][j-1];
                        b[i][j]=2;
                    }
                    else{
                        c[i][j]=c[i-1][j];
                        b[i][j]=3;
                    }
                }
            }
        }
    }
    /*
    我们在最长公共子序列长度c[i][j]的过程中,用b[i][j]记录了c[i][j]的来源,所以可以根据b[i][j]数组倒推最优解
     */
    static void print(int i,int j){//根据记录下来的信息构造最长公共子序列(从b[i][j]开始递推)
        if(i==0||j==0)
            return;
        if (b[i][j]==1){//说明s1[i-1]=s2[j-1],递归输出print(i-1,j-1);然后输出s1[i-1]
            print(i-1,j-1);
            System.out.print(s1[i-1]);
        }
        else if(b[i][j]==2){//说明s1[i-1]≠s2[j-1]且最优解来源于c[i][j]=c[i][j-1],递归输出print(i,j-1)
            print(i,j-1);
        }
        else//即b[i][j]=3,说明s1[i-1]≠s2[j-1]且最优解来源于c[i][j]=c[i-1][j],递归输出print(i-1,j)
            print(i-1,j);
    }
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int i,j;
        String s;
        System.out.println("请输入字符串s1:");
        s=sc.nextLine();
        len1=s.length();
        for(i=0;i<len1;i++){
            s1[i]=s.charAt(i);
        }
        System.out.println("请输入字符串s2:");
        s=sc.nextLine();
        len2=s.length();
        for(i=0;i<len2;i++){
            s2[i]=s.charAt(i);
        }
        for(i=0;i<=len1;i++){
            c[i][0]=0;//初始化第一列为0;
        }
        for(j=0;j<=len2;j++){
            c[0][j]=0;//初始化第一行为0;
        }
        LCSL();
        System.out.println("s1和s2的最长公共子序列的长度是:"+c[len1][len2]);
        System.out.println("s1和s2的最长公共子序列是:");
        print(len1,len2);
    }
}

运行示例

Java最长公共子序列递归 java求最长公共子序列_公共子序列_02

说明

我们可能发现s1和s2的最长公共子序列长度为4的,可能不止BADB,还有BADA,BCDA,BCDB,一开始我也说了,最长公共子序列不一定是唯一的,那么为什么读出来的最长公共子序列是这个而不是其他呢,这就和填表的时候有关了,填表的时,遇到字符不相等的时候,我们取那个位置的左侧和上面数值中的最大值,但是左侧和上面的数值相等的时候,我们默认取的是左侧的数字,代码是if(c[i][j-1]>=c[i-1][j]),没错就是那个等于号造成的,但是当左侧和上面的数值相等的时候,始终得默认它来源于一个地方,无论是左侧,还是上面,虽然会影响最长公共子序列的构成,但是一定不会影响最长公共子序列的长度,因为最长公共子序列本来就有可能不是唯一的。