算法设计技巧之动态规划(Java实现求最长公共子序列)

一、基本概念

每次决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

二、基本思想

基本思想:与分治法类似,也是将待求解的问题分解为若干个子问题,按顺序求解子问题,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解,依次解决各子问题,最后一个子问题就是初始问题的解。

与分治法最大的差别:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)

三、适用的情况

(1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

(2)无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响,也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。对每个子问题仅计算一次,把解保存在一个需要就可以查看的表中,而每次查表的时间为常数(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)。

四、解题步骤

动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。动态规划的设计都有着一定的模式,一般要经历以下几个步骤:

初始状态→│决策1│→│决策2│→…→│决策n│→结束状态

(1)划分阶段:按照问题的时间特征,把问题分为若干个阶段,在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来,当然,状态的选择要满足无后效性。

(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。

(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程。实际应用中可以按以下几个简化的步骤进行设计:

(1)找出最优解的性质,并刻画其结构特征。

(2)递归的定义最优解的值。

(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值(通常采用自底向上方法构造最优决策表)。

4)根据计算最优值时得到的信息,构造问题的最优解。

步骤(1)~(3)是动态规划算法的基本步骤。在只需要求出最优值的情形下,步骤(4)可以省略。若需要求出问题的一个最优解,则必须执行步骤(4),在步骤(3)中计算最优值时,通常需要记录更多的信息,以便在步骤(4)中根据所记录的信息快速构造出一个最优解。

五、算法实现的说明

动态规划的主要难点在于上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。使用动态规划求解问题,最重要的就是确定动态规划三要素

(1)问题的阶段

(2)每个阶段的状态

(3)从前一个阶段转化到后一个阶段之间的递推关系。

递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处

确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解

六、用动态规划解决最长公共子序列问题

1、【问题】 求两字符序列的最长公共字符子序列

问题描述:字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列,使得对所有的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的最长公共子序列。

2、求解思路

引进一个二维数组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]。

问题的递归式写成:

根据算法以自底向上方法构造最优决策表

3、算法分析:

由于每次调用至少向上或向左(或向上向左同时)移动一步,故最多调用(m + n)次就会遇到i = 0或j = 0的情况,此时开始返回。返回时与递归调用时方向相反,步数相同,故算法时间复杂度为Θ(m + n)

伪代码表示:

for x = 0 to n do
    for y = 0 to m do
        if (x == 0 || y == 0) then 
            LCS(x, y) = 0
        else if (Ax == By) then
            LCS(x, y) =  LCS(x - 1,y - 1) + 1
        else 
            LCS(x, y) = ) max(LCS(x – 1, y) , LCS(x, y – 1))
        endif
    endfor
endfor

4、Java实现

public class LCSTest {
    public static void main(String[] args) {
        int size=10;
        String x = generateRandomStr(size);
        String y = generateRandomStr(size);
        //1、定义最长公共子序列的长度
        int m = x.length();
        int n = y.length();
        //2、c[i][j]为最长公共子序列的长度,值为二维数组中的值
        int[][] c = new int[m+1][n+1];
        //3、初始化,第一行和第一列都是0
        for (int i = 0; i < m+1; i++) {
            c[i][0]=0;
        }
        for(int i=0;i<n+1;i++) {
            c[0][i]=0;
        }
        /**
         *    最优子结构路径
         * b[i][j]记录使得c[i][j]取最优值的最优子结构
         * b[i][j]=0表示c[i][j]可以取最优子结构,也就是x.chatAt[i-1]这个值属于最长公共子序列中
         * b[i][j]=1表示最优子结构存在c[i-1][j]中,c[i][j]不可以取得最优子结构
         * b[i][j]=2表示最优子结构存在c[i][j-1]中,c[i][j]不可以取得最优子结构
         */
        int[][] b = new int[m+1][n+1];
        //从第一行开始处理,一直处理到最后一行l[i][j]记录的是最长公共子序列的个数
        for(int i=1;i<=m;i++) {
            //每行都比较完每一列
            for (int j = 1; j <=n; j++) {
                //比较值是否相同
                if(x.charAt(i-1)==y.charAt(j-1)) {
                    //如果相同
                    c[i][j]=c[i-1][j-1]+1;
                    //表示x.charAt(i-1)这个为公共子序列的元素
                    b[i][j]=0;
                    //如果不相同,就表明该元素不属于公共子序列
                }else if(c[i-1][j]>=c[i][j-1]) {
                    //这里表明取得最长公共子序列是l[i-1][j]
                    c[i][j]=c[i-1][j];
                    b[i][j]=1;
                }else{
                    //这里表明取得最长公共子序列是l[i][j-1]
                    c[i][j]=c[i][j-1];
                    b[i][j]=2;
                }
            }
        }
        //这里输出c的值
        System.out.println("c:");
        for(int i=0;i<=m;i++) {
            //每行都比较完每一列
            for (int j = 0; j <=n; j++) {
                System.out.print(c[i][j]+"\t");
            }
            //换行
            System.out.println();
        }
        //输出b
        System.out.println("b:");
        for(int i=0;i<=m;i++) {
            //每行都比较完每一列
            for (int j = 0; j <=n; j++) {
                    System.out.print(b[i][j]+"\t");
            }
                    //换行
            System.out.println();
        }
        //输出最长公共子序列
        System.out.printf("%s与%s的最长公共子序列为:\n",x,y);
        printLCS(b,x,m,n);
    }
    /**
      *获取一个长度为length的字符串
     * @param length
     * @return
     */
    public static String generateRandomStr(int length) {
        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        Random random = new Random();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }
    /**
       *  输出一个最长公共子序列
     * @param b 最优子结构路径
     * @param x 序列
     * @param m 序列x的长度
     * @param n 序列y的长度
     */
    public static void printLCS(int[][] b,String x,int m,int n) {
        //只有值为0才是最长公共子序列的值
        if(m==0||n==0) {
            //终止
            return;
        }
        if(b[m][n]==0) {
            //这个值属于最长公共子序列,但是先输出之前的值
            printLCS(b,x,m-1,n-1);
            System.out.print(x.charAt(m-1));
        }else if(b[m][n]==1){
            //这个值不属于最长公共子序列
            printLCS(b,x,m-1,n);
        }else {
            printLCS(b,x,m,n-1);
        }
    }
}

运行结果如下:

c:
0    0    0    0    0    0    0    0    0    0    0    
0    0    1    1    1    1    1    1    1    1    1    
0    0    1    1    1    1    1    1    1    1    1    
0    0    1    1    1    1    1    1    1    1    1    
0    0    1    1    1    1    2    2    2    2    2    
0    0    1    1    1    1    2    2    2    2    2    
0    0    1    1    1    1    2    2    2    2    2    
0    1    1    1    1    1    2    2    2    2    2    
0    1    1    1    1    1    2    2    2    3    3    
0    1    1    1    1    1    2    2    2    3    3    
0    1    1    1    1    1    2    2    2    3    3    
b:
0    0    0    0    0    0    0    0    0    0    0    
0    1    0    2    2    2    2    2    2    2    2    
0    1    1    1    1    1    1    1    1    1    1    
0    1    1    1    1    1    1    1    1    1    1    
0    1    1    1    1    1    0    2    0    2    2    
0    1    1    1    1    1    1    1    1    1    1    
0    1    1    1    1    1    1    1    1    1    1    
0    0    1    1    1    1    1    1    1    1    1    
0    1    1    1    1    1    1    1    1    0    2    
0    1    1    1    1    1    1    1    1    1    1    
0    1    1    1    1    1    1    1    1    1    1    
ODMYTABHMQ与BORCCYSYHC的最长公共子序列为:
OYH

完成!

原文链接:,转发请注明来源!