1. 程式人生 > >JAVA動態規劃(二)--最長公共子序列問題(LCS_subSequence)的三種解法與最長公共子字串(LCS_subString)的兩種解法與最長迴文串(LongestPalindrome)

JAVA動態規劃(二)--最長公共子序列問題(LCS_subSequence)的三種解法與最長公共子字串(LCS_subString)的兩種解法與最長迴文串(LongestPalindrome)

動態規劃法

經常會遇到複雜問題不能簡單地分解成幾個子問題,而會分解出一系列的子問題。簡單地採用把大問題分解成子問題,並綜合子問題的解匯出大問題的解的方法,問題求解耗時會按問題規模呈冪級數增加。

為了節約重複求相同子問題的時間,引入一個數組,不管它們是否對最終解有用,把所有子問題的解存於該陣列中,這就是動態規劃法所採用的基本方法。

本文將介紹最長公共子序列問題(LCS_subSequence)的三種解法與最長公共子字串(LCS_subString)問題的兩種解法。

一、最長公共子子序列問題(LCS_subSequence)的三種解法
【問題】 求兩字元序列的最長公共字元子序列

問題描述:字元序列的子序列是指從給定字元序列中隨意地(不一定連續)去掉若干個字元(可能一個也不去掉)後所形成的字元序列。令給定的字元序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一個嚴格遞增下標序列[i0,i1,…,ik-1],使得對所有的j=0,1,…,k-1,有xij=yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一個子序列。

考慮最長公共子序列問題如何分解成子問題,設A=“a0,a1,…,am-1”,B=“b0,b1,…,bm-1”,並Z=“z0,z1,…,zk-1”為它們的最長公共子序列。不難證明有以下性質:

(1) 如果am-1=bn-1,則zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一個最長公共子序列;

(2) 如果am-1!=bn-1,則若zk-1!=am-1,蘊涵“z0,z1,…,zk-1”是“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一個最長公共子序列;

(3) 如果am-1!=bn-1,則若zk-1!=bn-1,蘊涵“z0,z1,…,zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一個最長公共子序列。

這樣,在找A和B的公共子序列時,如有am-1=bn-1,則進一步解決一個子問題,找“a0,a1,…,am-2”和“b0,b1,…,bm-2”的一個最長公共子序列;如果am-1!=bn-1,則要解決兩個子問題,找出“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一個最長公共子序列和找出“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一個最長公共子序列,再取兩者中較長者作為A和B的最長公共子序列。

求解:
引進一個二維陣列c[][],用c[i][j]記錄X[i]與Y[j] 的LCS 的長度,b[i][j]記錄c[i][j]是通過哪一個子問題的值求得的,以決定搜尋的方向。
我們是自底向上進行遞推計算,那麼在計算c[i,j]之前,c[i-1][j-1],c[i-1][j]與c[i][j-1]均已計算出來。此時我們根據X[i] = Y[j]還是X[i] != Y[j],就可以計算出c[i][j]。
問題的遞迴式寫成:
這裡寫圖片描述

演算法分析:由於每次呼叫至少向上或向左(或向上向左同時)移動一步,故最多呼叫(m * n)次就會遇到i = 0或j = 0的情況,此時開始返回。返回時與遞迴呼叫時方向相反,步數相同,故演算法時間複雜度為Θ(m * n)。

Java程式碼:

/*
 * 最長公共子序列問題
 * 方法(一):遞迴:返回最長子序列長度
* */
public class LCS_problem {
    public static void main(String[] args) {
        String s1="abcdefghijkl";
        String s2="cdafu";
        int l=new LCS_problem().lcs(s1, s1.length()-1, s2, s2.length()-1);
        System.out.println(l);
    }

    public int lcs(String s1,int x,String s2,int y){
        if(x<0||y<0){
            return 0;
        }
        if(s1.charAt(x)==s2.charAt(y)){
            return lcs(s1,x-1,s2,y-1)+1;   //如果末位相同,則子序列長度加1
        }else{  //否則比較兩個字串分別去掉一個字元的子序列長度
            int l1=lcs(s1,x-1,s2,y);
            int l2=lcs(s1,x,s2,y-1);
            return l1>l2?l1:l2;
        }
    }
}

遞迴只能輸出最長子序列的長度,不能輸出具體的子序列,下面我們看看如何用動態規劃的方法輸出最長公共子序列:

/*
 * 最長公共子序列問題
 * 動態規劃【方法一:對開始字元初始狀態單獨處理】:返回最長子序列長度int 以及具體的子序列String
* */
public class LCS_problem {
    public static void main(String[] args) {
        String s1="abcdefghijkl";
        String s2="1b2ck3";
        char []x=s1.toCharArray();
        char []y=s2.toCharArray();
        int b[][]=new LCS_problem().getLength(x,y);
        new LCS_problem().subSequence(b,x,x.length-1,y.length-1);
    }
    public int[][] getLength(char[] x,char[] y){
        int l1=x.length;
        int l2=y.length;
        /*引進一個二維陣列c[][],用c[i][j]記錄X[i]與Y[j] 的LCS 的長度,
         * b[i][j]記錄c[i][j]是通過哪一個子問題的值求得的,以決定搜尋的方向。*/
        int[][]c=new int[l1][l2];
        int[][]b=new int[l1][l2];
        for(int i=0;i<l1;i++){
            for(int j=0;j<l2;j++){
                if(i==0||j==0){ //處理i=0或者j=0的時候,陣列下標不能-1的問題,並且此時c[i][j]最大為1
                    if(x[i]==y[j]){
                        c[i][j]=1;
                      b[i][j]=1;
                    }
                    else if(j>0){
                        c[i][j]=c[i][j-1];
                      b[i][j]=-1;
                    }
                    else if(i>0){
                        c[i][j]=c[i-1][j];
                      b[i][j]=0;
                    }
                }
                else if(x[i]==y[j]){
                    c[i][j]=c[i-1][j-1]+1;
                    b[i][j]=1;
                }
                else if(c[i-1][j]>=c[i][j-1]){
                    c[i][j]=c[i-1][j];
                    b[i][j]=0;
                }
                else{
                    c[i][j]=c[i][j-1];
                    b[i][j]=-1;
                } 
            }
        }
        System.out.println("The length of sublsequence="+c[l1-1][l2-1]);
        return b;
    }

    public void subSequence(int[][]b,char[]x,int m,int n){//m,n分別為s1,s2的長度
        if(m<0||n<0){
            return;
        }
        if(b[m][n]==1){
            subSequence(b,x,m-1,n-1);
            System.out.print(x[m]+"\t");
        }
        else if(b[m][n]==0){
            subSequence(b,x,m-1,n);
        }
        else{
            subSequence(b,x,m,n-1);
        }
}

當然,這裡的程式碼重複度有點高,因為對字元的初始狀態做了單獨的處理,我們還可以通過對字串做處理,減少程式碼重複度。

/*
 * 最長公共子序列問題
 * 動態規劃:返回最長子序列長度int 以及具體的子序列String
【方法二:字串前面加空格,使第一個字元和後面的字元可以統一處理】
* */
public class LCS_problem {
    public static void main(String[] args) {
        String s1="abcdefghijkl";
        String s2="1b2ck3";
//前面加空格的原因是為了把字串第一個字元和後面的字元統一處理,不用單獨處理
        char []x=(" "+s1).toCharArray();
        char []y=(" "+s2).toCharArray();
        int b[][]=new LCS_problem().getLength(x,y);
        new LCS_problem().subSequence(b,x,x.length-1,y.length-1);
    }

    public int[][] getLength(char[] x,char[] y){
        int l1=x.length;
        int l2=y.length;
        /*引進一個二維陣列c[][],用c[i][j]記錄X[i]與Y[j] 的LCS 的長度,
         * b[i][j]記錄c[i][j]是通過哪一個子問題的值求得的,以決定搜尋的方向。*/
        int[][]c=new int[l1][l2];
        int[][]b=new int[l1][l2];

        for(int i=1;i<l1;i++){
            for(int j=1;j<l2;j++){
                if(x[i]==y[j]){
                    c[i][j]=c[i-1][j-1]+1;
                    b[i][j]=1;
                }
                else if(c[i-1][j]>=c[i][j-1]){
                    c[i][j]=c[i-1][j];
                    b[i][j]=0;
                }
                else{
                    c[i][j]=c[i][j-1];
                    b[i][j]=-1;
                } 
            }
        }
        System.out.println("The length of sublsequence="+c[l1-1][l2-1]);
        return b;
    }

    public void subSequence(int[][]b,char[]x,int m,int n){//m,n分別為s1,s2的長度
        if(m<0||n<0){
            return;
        }
        if(b[m][n]==1){
            subSequence(b,x,m-1,n-1);
            System.out.print(x[m]+"\t");
        }
        else if(b[m][n]==0){
            subSequence(b,x,m-1,n);
        }
        else{
            subSequence(b,x,m,n-1);
        }
    }

二、最長公共子字串(LCS_subString)問題的兩種解法

package dynamic_programming;
/**
 * @author Gavenyeah
 *
 * @date Time: 2016年4月3日下午12:45:59
 */
//查詢最長公共子字串的長度和輸出子字串
public class LCS_subString {
    public static void main(String[] args) {
        String s1="1234abc22";
        String s2="1212345abc22";
        char[]x=(" "+s1).toCharArray();
        char[]y=(" "+s2).toCharArray();
        int[][]c=new LCS_subString().getLength(x, y);
        System.out.print("方法一-->動態規劃求解:");
        new LCS_subString().lcs_subString(c,x,x.length,y.length);
        System.out.println();
        System.out.println("**********************************************");
        System.out.print("方法二-->陣列比較求解:");
        new LCS_subString().lcs_subString(x,y);
    }

    //方法一-->動態規劃求解
      /*動態轉移方程為:
       如果xi == yj, 則 c[i][j] = c[i-1][j-1]+1
       如果xi ! = yj,  那麼c[i][j] = 0*/
    public void lcs_subString(int[][]c,char[] x,int m,int n){
        int max=0;
        int p=0;
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(max<c[i][j]){
                    max=c[i][j];
                    p=i;
                }
            }
        }
        System.out.println("The length of LCS_subString:"+max);
        while(max>0){
            max--;
            System.out.print(x[p-max]);
        }
    }
    public int[][]getLength(char[]x,char[]y){
        int[][]c=new int[x.length][y.length];
        for(int i=1;i<x.length;i++){
            for(int j=1;j<y.length;j++){
                if(x[i]==y[j]){
                    c[i][j]=c[i-1][j-1]+1;
                }
                else{
                    c[i][j]=0;
                }
            }
        }
        return c;
    }


    //方法二-->陣列比較求解
    public void lcs_subString(char x[],char y[]){
        int len=0;//記錄最長子字串的長度
        int loc=0;//記錄最長子字串開始的位置
        for(int i=0;i<x.length;i++){
            for(int j=0;j<y.length;j++){
                int count=0;//記錄當前比較的子字串的長度
                int m=i,n=j;
                while(m<x.length&&n<y.length&&x[m]==y[n]){
                    count++;
                    m++;
                    n++;
                }
                if(len<count){
                    len=count;
                    loc=i;
                }
                if(count>0)   //加快查詢速度
                    j=j+count-1;
            }
        }
        System.out.println("The length of LCS_subString:"+len);
        while(len>0){
            System.out.print(x[loc]);
            len--;
            loc++;
        }
    }
}

3.最長迴文字串的兩種解法
最長迴文字串即類似:aba,abba等這些形式的。

package dynamic_programming;
/**
 * @author Gavenyeah
 *
 * @date Time: 2016年4月3日下午5:53:58
 */
public class LongestPalindrome {
    public static void main(String[] args) {
        String s="123321abccba12332";
//      new LongestPalindrome().palindrome(s);
        new LongestPalindrome().getPalindrome(s);
    }

    //將可能是迴文串的字元取出進行比較
    public void getPalindrome(String s){
        String longestPa=s.substring(0,1);//附初值,以及當s的長度為1的時候的返回值
        int maxLen=0;
        for(int i=0;i<s.length();i++){
            for(int j=i+1;j<s.length();j++){
                if(s.charAt(i)==s.charAt(j)){
                    String pa=s.substring(i,j+1);
                    int len=j-i+1;
                    if(maxLen<len&&isPalindrome(pa,len)){
                        maxLen=len;
                        longestPa=pa;
                    }
                }
            }
        }
        System.out.println(longestPa);
    }
    public boolean isPalindrome(String pa,int len){
        for(int i=0,j=len-1;i<=j;i++,j--){
            if(pa.charAt(i)!=pa.charAt(j)){
                return false;
            }
        }
        return true;
    }


    /*
     //逐步向後查詢法
      public void palindrome(String s){
        if(s.length()==1||(s.length()==2&&s.charAt(0)==s.charAt(1))){
            System.out.print(s);
            return;
        }
        char c[]=s.toCharArray();
        int max=0;
        int loc=0;
        for(int i=2;i<c.length;i++){
            int count=0;
            int k=i;
            int j=i-1;
            if(c[k]==c[j]){
                while((k<c.length&&j>=0)&&c[k]==c[j]){
                    count++;
                    k++;
                    j--;
                }
                count*=2;
            }
            else if(c[k]==c[j-1]){
                while((k<c.length&&j>=0)&&c[k]==c[j-1]){
                    count++;
                    k++;
                    j--;
                }
                count=count*2+1;
            }
            if(max<count){
                max=count;
                if(max%2==0){
                    loc=j+1;
                }
                else loc=j;
            }
        }
        int k=1;
        while(k<=max){
            System.out.print(c[loc]);
            loc++;
            k++;
        }
    }*/
}