算法导论学习笔记(六):动态规划

转载请注明原作者 @yoshino,强烈要求支持TOC目录和数学公式的输入!
更新不及时,有空慢慢写吧,原文是写在Bear上粘贴来的,可能格式有点乱。
具体的工程在我的github上。


动态规划(dynamic programming,DP),与分治策略相似,通过求解子问题来求解原问题,不同于分治策略的事DP的子问题是相互重叠的,DP通常是用来求解最优化问题,设计一个动态规划算法步骤:

  1. 刻画一个最优解的结构特征
  2. 递归地定义最优解的值
  3. 计算最优解的值,通常采用自底向上的方法
  4. 利用计算出的信息构造一个最优解

钢条切割

算法导论学习笔记(六):动态规划_第1张图片
钢条切割问题描述

求解思路

对于一根程度为l的钢条,有两种不同的切割法:

  • 不切割:此时的收益为rl=pl
  • 切割成长度i和l-i两部分:此时收益用递归表示:rl=ri+l-i
    最大收益可以写成:p=max(pl,ri+l-i,…,r1+l-1)
    即取其中最大的

两种动态规划算法:

  • 带备忘的自顶向下算法:按照自然递归的形式编写过程,过程中保留之前子问题的解,其中会检查是否已经保过此解,如果已经保存了就直接返回保存的值,从而节省了计算的时间。
    public int memoizedCutRodAux(int price[], int r[], int l) {//带备忘的自顶向下法
        if (r[l] > 0) {
            return r[l];//r[l]用来存储当长度为l时的最优解,price用来存储题目给出的收益值
        } else {
            int profit = 0;
            for (int i = 1; i <= l; i++) {
                profit=Math.max(profit,price[i]+memoizedCutRodAux(price,r,l-i));
            }
            r[l]=profit;//保存最优解
            return profit;
        }
    }
  • 自底向上法:任何子问题的求解都依赖于更小子问题的求解,把子问题按照规模大小进行排序,按从大到小的顺序进行求解,这样求解时保证每个求解的问题的前提子问题都已经求解过了
public int bottomUpCutRod(int price[], int r[], int l) {
        for (int i = 1; i <= l; i++) {
            int profit = 0;
            for (int j = 1; j <= i; j++) {
                profit = Math.max(profit, price[j] + r[i - j]);
            }
            r[i] = profit;
        }
        return r[l];
    }

最长公共子序列(longest-common-subsequence,LCS)

问题描述

给定两个公共子序列X=和Y=,求X Y的长度最长的公共子序列,子序列不要求要连续,但是顺序要相同

问题分析

  1. 刻画LCS特征,定义前缀对:


    算法导论学习笔记(六):动态规划_第2张图片
    前缀对

    从定理15.1可以得出,两个序列的LCS包含两个序列的前缀的LCS。

  2. 得出一个递归解
    从定理15.1可以知道:
  • 若xm=yn,则求解Xm-1和Yn-1的LCS,将xm=yn至于后面即可
  • 若xm不等于yn,则求解Xm-1和Yn的LCS和Xm和Yn-1的LCS,取其中较长的作为LCS
    之所以能做这样的假设,是因为最优解必然出现在X和Y中。
    算法导论学习笔记(六):动态规划_第3张图片
    LCS递归公式
  1. 计算LCS的长度
  • 建立一个矩阵c[0...m,0...n]用来保存c[i,j]的值
  • 建立一个表b[1...m,1...n]用来保存最优解,即b[i,j]保存c[i,j]的子问题最优解
  • java代码实现
private int[][] LCSLength(String[] X, String[] Y) {
        ArrayList result = new ArrayList();
        int m = X.length;
        int n = Y.length;
        int[][] c = new int[m + 1][n + 1];
        int[][] b = new int[m + 1][n + 1];
        for (int i = 1; i < m; i++)
            for (int j = 0; j < n + 1; j++) {
                c[i][j] = 0;
            }
        for (int i = 1; i <= m; i++)
            for (int j = 1; j <= n; j++) {
                if (X[i - 1] == Y[j - 1]) {//为了照顾c的序号,所以所有的X,Y均-1了
                    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] = 2;//代表向上箭头
                } else {
                    c[i][j] = c[i][j - 1];
                    b[i][j] = 3;//代表向左箭头
                }
            }
        return b;
    }
  1. 构造LCS
    从第三步我们得到的b[i,j],我们得到了构造LCS的方向,递归回溯打印LCS即可
    private void printLCS(int[][] b, String[] X, int i, int j) {
        if (i != 0 && j != 0) {
            if (b[i][j] == 1) {
                printLCS(b, X, i - 1, j - 1);
                System.out.print(X[i-1]);
            } else if (b[i][j] == 2) {
                printLCS(b, X, i - 1, j);
            } else {
                printLCS(b, X, i, j - 1);
            }
        }
    }

最优二叉搜索树

问题描述

给定一个n个不同关键字的已排序的序列K=(递增序列),对于每个关键字ki,都有一个概率pi表示其搜索频率。当然有些搜索的值可能不在K中,所以我们给出n+1个伪关键字“d0,d1,…,dn”,其中d0表示所有小于k1的值,dn表示所有大于kn的值,对于每个di,也有相应的概率pi

你可能感兴趣的:(算法导论学习笔记(六):动态规划)