数据结构与算法之动态规划(一)最长公共子序列和最长公共子串

引言

所谓动态规划(Dynamic Programming),是一种分阶段求解问题的数学思想。它不止应用于编程领域,也应用于管理学、经济学等其他行业,通俗的讲,它的思想就是"大事化小,小事化了"。为了对它有初步认识,我们先举一个入门的例子,来说明它的基本思想。

走台阶组合问题

有一座10级台阶的楼梯,从下往上走,一次只能向上走1或者2个台阶,问一共有多少走法?
穷举法明显效率低下,我们不妨先这样考虑:假设就差最后一步走完台阶,这时候会出现两种情况:
1>在第8台阶上跨两台阶走完;
2>在第9台阶上跨一台阶走完.
接下来我们暂且不考虑我们怎么走到第8、9台阶,我们只需要知道它们已经求解,分别有X、Y种走法,那么此时有迭代式:
F(10) = X+Y = F(8) + F(9)
同样的思路,如何走到第8、9台阶的组合也是由第6、7和7、8台阶而来:
F(8) = F(6)+F(7)
F(9) = F(7)+F(8)
依次递推,可以将规模为10的问题进行瘦身,瘦身到什么时候呢?
当到到F(1)和F(2)时,只需要一步就可以到达第1、2台阶,所以有:
F(1) = 1, F(2)=2
这就是迭代的边界条件。因此动态规划迭代公式和边界条件为:
F(1) = F(2) = 1;
F(n) = F(n-1) + F(n-1), n>2;
这里F(n-1)和F(n-1)为最优子结构,F(n) = F(n-1) + F(n-1)为状态转移方程,F(1) = F(2) = 1为求解边界,在非递归算法中它也是迭代起点。
我们发现这是斐波那契数列,哈哈,一个递归不就解决了吗?是的,问题 是可以求解,但是这样做,产生大量重复性计算,时间复杂度为为2的n次方,效率低下。如何解决效率问题呢?迭代公式反应了自顶向下的关系,而动态规划是从底向上迭代(如果需要的话,需要将中间计算结果保存在张表中)。最终的动态规划代码实现如下:

package com.qicode.algorithmlearning.dp;

import java.util.ArrayList;

/**
 * Created by chenming on 2018/6/21
 * 走台阶问题,有n个台阶,每一步只能走1或者2台阶,求解走法个数和走法
 * 动态规划模型:
 * F(n) = F(n-1) + F(n-2)
 * F(1) = 1;
 * F(2) = 2;
 */
public class StepCombination {
    public static void stepCombination(int n) {
        int result = 0;
        //保存
        ArrayList<String> preWays = new ArrayList<>();//F(n-1)的结果
        ArrayList<String> prepreWays = new ArrayList<>();//F(n-2)的结果
        ArrayList<String> resultWays = new ArrayList<>();//F(n)的结果
        if (n == 1) {
            result = 1;
            resultWays.add("1");
            return;
        }
        if (n == 2) {
            result = 2;
            resultWays.add("11");
            resultWays.add("2");
            return;
        }

        //开始迭代
        int prepre = 1;
        int pre = 2;
        prepreWays.add("1");
        preWays.add("2");
        preWays.add("11");//到达第二台阶的所有结果
        for (int i = 3; i <= n; i++) {
            result = pre + prepre;
            prepre = pre;
            pre = result;
            //处理结果prepreWays的所有元素+2,preWays的所有元素+1,俩集合并入resultWays
            resultWays = new ArrayList<>();//new resultWays 存放新的结果
            for (int m = 0; m < prepreWays.size(); m++) {
                String s = prepreWays.get(m);
                StringBuilder sb = new StringBuilder(s);
                sb.append("2");
                s = sb.toString();
                resultWays.add(s);
            }

            for (int m = 0; m < preWays.size(); m++) {
                String s = preWays.get(m);
                StringBuilder sb = new StringBuilder(s);
                sb.append("1");
                s = sb.toString();
                resultWays.add(s);
            }
            //下一次迭代
            prepreWays = preWays;
            preWays = resultWays;
        }

        System.out.println("台阶步数:"+result);
        System.out.println("======台阶组合====");
        for(int i = 0 ; i < resultWays.size();i++){
            System.out.println(resultWays.get(i));
        }
    }
}

打印结果:

台阶步数:8
======台阶组合====
122
212
1112
221
1121
1211
2111
11111

到此为止,一个最最简单的动态规划问题求解完毕。下面我们再继续研究今天的主题:最长公共子序列和最长公共子串。其中后者的求解是在前者的基础之上,我们先看最长公共子序列问题

最长公共子序列

例:X="GHHABDCAB"和Y=“BDCAMBAE”,最长公共子序列为:BDCAB。注意最长公共子序列不连续。如果使用穷举法,一个长度为n的序列的组合为2的n次幂,事件复杂度为指数阶,这可是唯恐天下不乱的爆炸性时间复杂度。那么这个问题可不可以采用动态规划呢?
先分析它的最优子结构.假设,已经知道Z(k)={z1, z2, z3, ... zk}为X(m) = {x1,x2....xm}和Yn={y1,y2...yn}的最大公共子序列,有以下三种结论
1>,x[m]=y[n]=z[k],那么去除掉他们的子序列Z(k-1)是X(m-1)和Y(n-1)的最长公共子序列;
2>x[m]!=y[n], x[m]!=z[k], y[n]=z[k]:我们可以把x[m]去掉,那么Z(k)是X(m-1)和Y(n)的最长公共子序列;
3>y[n]!=x(m),y[n]1=z[k],我们可以把y[n]去掉,那么Z(k)是X(m)和Y(n-1)的最长公共子序列.
依据以上结论,最长公共子序列的递推公式:



其中C[i][j]记录最长公共子序列的长,i和j分别为XY串中的索引.
然后,在寻找边界:当i=0或者j=0,表示X或者Y中一个子串为空,C[i][j]=0;
最后,制定数据结构和算法:
1>二维数组C[m][n]记录迭代结果,b[i][j]记录最长子串的生成方向,如C[i][j] = C[i-1][j-1]+1,则b[i][j]记录为左上方向,其他同理,运算完毕,可以根据b[][]记录的方向逆向得到最长子序列;
2>从i=0,j=0开始按照迭代公式填充C[i][j]和b[i][j],初始状态i-1,j-1越界,此时默认越界的元素为0.
3>迭代完毕,C[m-1][n-1]记录了最长公共序列的长度,b[i][j]记录了最长子串迭代的"方向",逆向遍历b[][]可得到一个最长公共子序列。

package com.qicode.algorithmlearning.dp;

import stack.Stack;

/**
 * Created by chenming on 2018/6/19
 * 动态规划-最长公共子序列
 * 递推公式
 * 1.C(i, j) = C(i-1, j-1)+1  x[i] = y[j]
 * 2.C(i, j) = Max(C(i-1, j),C(i, j-1)) x[i] != y[j]
 * 3.C(i, j) = 0 i=0或j=0
 */
public class LCS {
    private char[] s1, s2;//俩序列
    private int len1, len2;
    private int[][] c;//动态规划运算记录表
    private int[][] b;//记录子序列生成路径,b[i][j] = 1表示情况1,来源于左上,b[i][j]=2,来源于左,b[i][j]=3来源于上
    private final int TYPE_LEFT_TOP = 1;//左上
    private final int TYPE_LEFT = 2;//左
    private final int TYPE_TOP = 3;//上

    public LCS(String s1, String s2) {
        this.s1 = s1.toCharArray();
        this.s2 = s2.toCharArray();
        len1 = s1.length();
        len2 = s2.length();

        c = new int[len1][len2];
        b = new int[len1][len2];
    }

    /**
     * 最长公共子序列迭代
     */
    public void getLcs() {
        int i, j;//行为s1,列为s2

        //开始迭代
        for (i = 0; i < len1; i++) {
            for (j = 0; j < len2; j++) {
                if (s1[i] == s2[j]) {
                    c[i][j] = itemWithBoundary(i - 1, j - 1) + 1;
                    b[i][j] = TYPE_LEFT_TOP;
                } else {
                    int leftItem = itemWithBoundary(i, j - 1);
                    int topItem = itemWithBoundary(i - 1, j);
                    if (leftItem >= topItem) {
                        c[i][j] = leftItem;
                        b[i][j] = TYPE_LEFT;
                    } else {
                        c[i][j] = topItem;
                        b[i][j] = TYPE_TOP;
                    }
                }
            }
        }
        dumpMatrix();
        dumpLcs1();

        dumpAllLcs();
    }

    /**
     * 超出边界返回0 否则返回c[i][j]
     */
    private int itemWithBoundary(int i, int j) {
        if (i < 0 || j < 0 || i >= len1 || j >= len2) {
            return 0;
        }
        return c[i][j];
    }

    /**
     * 借助b[][],输出Lcs
     */
    private void dumpLcs() {
        System.out.println("=======最小公共子序列=========");
        int i = len1 - 1;
        int j = len2 - 1;
        Stack<Character> stack = new Stack<>();
        while (i >= 0 && j >= 0) {
            if (b[i][j] == TYPE_LEFT_TOP) {//方向左上
                stack.push(s1[i]);
                i--;
                j--;
            } else if (b[i][j] == TYPE_LEFT) {//方向向左
                j--;
            } else if (b[i][j] == TYPE_TOP) {//方向向上
                i--;
            }
        }
        //输出最长子序列
        StringBuilder result = new StringBuilder();
        while (!stack.isEmpty()) {
            Character pop = stack.pop();
            result.append(pop);
        }

        System.out.println(result.toString());
    }

    /**
     * 打印迭代矩阵
     */
    private void dumpMatrix() {
        for (int i = 0; i < len1; i++) {
            for (int j = 0; j < len2; j++) {
                System.out.print(c[i][j] + ", ");
            }
            System.out.println("");
        }
    }
}

说明: dumpLcs方法是借助数组b,由于C[][]记录了最长子序列的变化,因此也可以得出它的方向。优化的代码如下:

  /**
     * 不借助辅助数组b[][],c[][]中隐含了LCS信息
     * 1.c[i][j] 字符相等,则输出
     * 2.字符不相等,则判断c[i][j-1]和c[i-1][j]的大小,向较大者方向遍历
     */
    private void dumpLcs1() {
        System.out.println("=======最小公共子序列=========");
        int i = len1 - 1;
        int j = len2 - 1;
        Stack<Character> stack = new Stack<>();
        while (i >= 0 && j >= 0) {
            if (s1[i] == s2[j]) {
                stack.push(s1[i]);
                i--;
                j--;
            } else {
                int left = itemWithBoundary(i, j - 1);
                int top = itemWithBoundary(i - 1, j);
                if (left >= top) {
                    //向左遍历
                    j--;
                } else {
                    i--;
                }
            }
        }

        StringBuilder result = new StringBuilder();
        while (!stack.isEmpty()) {
            Character pop = stack.pop();
            result.append(pop);
        }

        System.out.println(result.toString());

    }

最长公共子序列可能有多个,如下图

多个最长公共子序列.png

当X[i]!=Y[j]且C[i-1][j] == C[i][j-1]时,说明C[i][j]的生成有两个方向,此时分别从C[i-1][j]和C[i][j-1]递归逆向查找。

 /**
     * 打印所有
     */
    public void dumpAllLcs() {
        System.out.println("=====所有LCS=====");
        StringBuilder sb = new StringBuilder();
        int i = len1 - 1;
        int j = len2 - 1;
        dumpLcsRes(i, j, sb);
    }

    /**
     * 递归打印
     *
     * @param sb
     */
    private void dumpLcsRes(int i, int j, StringBuilder sb) {
        sb = new StringBuilder().append(sb.toString());//新建stringbuilder,暂时存放当前结果
        while (i >= 0 && j >= 0) {
            if (s1[i] == s2[j]) {
                sb.append(s1[i]);
                i--;
                j--;
            } else {
                int left = itemWithBoundary(i, j - 1);
                int top = itemWithBoundary(i - 1, j);
                if (left > top) {
                    //向左遍历
                    j--;
                } else if (left < top) {//向上遍历
                    i--;
                } else {//c[i-1][j] == c[i][j-1],递归从[i-1][j]和[i][j-1]开始
                    dumpLcsRes(i - 1, j, sb);
                    dumpLcsRes(i, j - 1, sb);
                    return;
                }
            }
        }

        System.out.println(sb.toString());
    }

测试代码:

 /**
     * 测试最长公共子序列
     */
    @Test
    public void testLcs(){
        LCS lcs = new LCS("BDCABA", "ABCBDAB");
        lcs.getLcs();
//        lcs.getLcss();
    }

打印如下:

0, 1, 1, 1, 1, 1, 1, 
0, 1, 1, 1, 2, 2, 2, 
0, 1, 2, 2, 2, 2, 2, 
1, 1, 2, 2, 2, 3, 3, 
1, 2, 2, 3, 3, 3, 4, 
1, 2, 2, 3, 3, 4, 4, 
=======最小公共子序列=========
BCBA
=====所有LCS=====
BADB
BACB
ABCB

最长公共子串

最长公共子串和序列不同的是,它是连续的,所以当X[i]!=Y[j]时,需要从0开始重新扫描。其它思路和步骤和最长公共子串基本相同:

 /**
     * 在最长公共子序列的基础上,实现最长公共字串
     * <p>
     * 递推公式
     * 1.C(i, j) = C(i-1, j-1)+1  x[i] = y[j]
     * 2.C(i, j) = 0 x[i] != y[j]
     * 3.C(i, j) = 0 i=0或j=0
     */
    public void getLcss() {
        //初始化第一行
        int i, j;//行为s1,列为s2
        int maxSubLen = 0;
        int maxX = 0, maxY = 0;//记录最长子串的结束位置
        for (i = 0; i < len1; i++) {
            for (j = 0; j < len2; j++) {
                if (s1[i] == s2[j]) {
                    c[i][j] = itemWithBoundary(i - 1, j - 1) + 1;
                } else {
                    c[i][j] = 0;
                }

                if (maxSubLen < c[i][j]) {
                    maxSubLen = c[i][j];
                    maxX = i;
                    maxY = j;
                }
            }
        }
        //打印迭代记录表C[][]
        dumpMatrix();
        System.out.println("最长公共子序列结束位置(x, y):" + maxX + ":" + maxY);
        dumpMaxLenSubString(maxX, maxY);
    }

    /**
     * 打印最长公共子串
     * 从[x,y]位置开始向左上遍历直到c[i][j]越界,或者c[i][j]==0
     *
     * @param x
     * @param y
     */
    private void dumpMaxLenSubString(int x, int y) {
        Stack<Character> stack = new Stack<>();
        int i = x;
        int j = y;
        while (i >= 0 && j >= 0 && c[i][j] > 0) {
            stack.push(s1[i]);//直接向左上遍历
            i--;
            j--;
        }
        //输出最长子串
        StringBuilder result = new StringBuilder();
        while (!stack.isEmpty()) {
            Character pop = stack.pop();
            result.append(pop);
        }

        System.out.println("最长公共子串为:" + result.toString());
    }

由于只有当X[i]=Y[j]时才继续迭代,所以打印最长字串只要从字串的结束位置,向左上遍历知道C[i][j]=0为止。打印结果为:

0, 1, 0, 1, 0, 0, 1, 
0, 0, 0, 0, 2, 0, 0, 
0, 0, 1, 0, 0, 0, 0, 
1, 0, 0, 0, 0, 1, 0, 
0, 2, 0, 1, 0, 0, 2, 
1, 0, 0, 0, 0, 1, 0, 
最长公共子序列结束位置(x, y):1:4
最长公共子串为:BD

最长公共子串也可能有多个,找到最大的字串结束位置,放入集合,然后遍历集合执行dumpMaxLenSubString即可,逻辑比较简单,代码略过。
完整代码地址:数据结构与算法学习JAVA描述GayHub地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 回溯算法 回溯法:也称为试探法,它并不考虑问题规模的大小,而是从问题的最明显的最小规模开始逐步求解出可能的答案,并...
    fredal阅读 13,508评论 0 89
  • 动态规划(Dynamic Programming) 本文包括: 动态规划定义 状态转移方程 动态规划算法步骤 最长...
    廖少少阅读 3,187评论 0 18
  • 原文在这里 http://blog.csdn.net/dq_dm/article/details/45043689...
    superczb阅读 1,058评论 0 1
  • 与西宁一起到上海路参加一个她男友朋友们的小型交流会。没走进上海路附近小巷里多久,就看到西宁男友向我们挥着手。我们沿...
    游客的音文阅读 448评论 0 1
  • 黑执事·那个执事,真爱【同人/耽美/不喜勿入】 入黑执事的坑已经四年了,很喜欢塞夏这对别扭的主仆,今天我们来讲述他...
    秦云渚阅读 384评论 0 2