算法导论 第四部分——基本数据结构——第15章:动态规划

前言:动态规划的概念

  动态规划(dynamic programming)是通过组合子问题的解而解决整个问题的。分治算法是指将问题划分为一些独立的子问题,递归的求解各个问题,然后合并子问题的解而得到原问题的解。例如归并排序,快速排序都是采用分治算法思想。本书在第二章介绍归并排序时,详细介绍了分治算法的操作步骤,详细的内容请参考:http://www.cnblogs.com/Anker/archive/2013/01/22/2871042.html。而动态规划与此不同,适用于子问题不是独立的情况,也就是说各个子问题包含有公共的子问题。如在这种情况下,用分治算法则会重复做不必要的工作。采用动态规划算法对每个子问题只求解一次,将其结果存放到一张表中,以供后面的子问题参考,从而避免每次遇到各个子问题时重新计算答案。

动态规划与分治法之间的区别:
(1)分治法是指将问题分成一些独立的子问题,递归的求解各子问题
(2)动态规划适用于这些子问题不是独立的情况,也就是各子问题包含公共子问题

  动态规划通常用于最优化问题(此类问题一般有很多可行解,我们希望从这些解中找出一个具有最优(最大或最小)值的解)。动态规划算法的设计分为以下四个步骤:

(1)描述最优解的结构

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

(3)按自低向上的方式计算最优解的值

(4)由计算出的结果构造一个最优解

  动态规划最重要的就是要找出最优解的子结构。书中接下来列举4个问题,讲解如何利用动态规划方法来解决。动态规划的内容比较多,我计划每个问题都认真分析,写成日志。

一、钢条切割

已知不同长度的钢条的价格,如下图.给定一个长度为n的钢条,如何切分才能使卖出去的价格最高。

  

方法1: 自顶向下的递归实现  : 该方法的弊端是: 复杂度为 2^n  因为每次回次调用都会递归调用子序列,而不会将重复计算的数据保存起来  

cut_rod(p,n)
    if n==0
        return 0
    q=int_min
    for i= 1 to n
        q=max(q,p[i]+cut_rod(p,n-i))
    return q;
int cut_rod(int *p, int len, int n)
{
    if (n == 0)
        return 0;
    int q = MIN;
    *p = 0;
    for (int i = 1; i <= n; i++)
    {
        q = max(q, *(p + i) + cut_rod(p, len, n - i));
    }
    return q;
}

方法2: 带备忘录的自顶向下的递归实现  : 是对方法一的改进

memoized-cut-rod(p,n)
    let r[0...n] be new array
    for i=0 to n
        r[i] = int_min
    return memoized-cut-aux(p,n,r)

memoized-cut-aux(p,n,r)
    if r[n] >= 0
        return r[n]
    if n==0
        q=0
    else
        for i 1 to n
            q=max(q,p[i]+memoized-cut-aux(p,n-i,r))
        r[n] = q
    return q
int mem_cut_rod(int *p, int len, int n)
{
    vector<int> result;
    result.push_back(0);
    for (int i = 0; i<n + 1; i++)
        result.push_back(MIN);
    return mem_cut_rod_r(p, len, n, result);
}
int mem_cut_rod_r(int *p, int len, int n, vector<int> &result)
{

    if (result.at(n) >= 0)
        return result.at(n);

    int q;

    for (int i = 1; i<n + 1; i++)
    {
        q = MIN;
        for (int j = 1; j <= i; j++)
        {
            q = max(q, *(p + j) + mem_cut_rod_r(p, len, i - j, result));
        }
        result.at(i) = q;
    }
    return q;
}

方法三、自底向上的实现

伪代码:

bottom-cut-rod(p,n)
    let r[0....n] be a new array
    r[0] =0
    for j=1 to n
        q=int_min
        for i= 1 to j
            q=max(q,p[i]+r[j-i])
        r[i] = q
    return r[n] 

具体实现:  

int bottom_up_rod(int *p, int len, int n)
{
    if (n == 0 || n == 1)
        return n == 0 ? 0 : *(p + 1);
    int *result = new int[n + 1];
    int  q;
    *result = 0;
    for (int i = 1; i<n + 1; i++)
        *(result + i) = MIN;
    for (int i = 1; i <= n; i++)
    {
        q = MIN;
        for (int j = 1; j <= i; j++)
        {
            q = max(q, *(p + j) + bottom_up_rod(p, len, i - j));
        }
        *(result + i) = q;
    }
    int temp = *(result + n);
    delete[]result;
    return temp;
}

二、矩阵链乘法

  矩阵的成法有结合律,那么问题来了,怎么结合才能使 运算的次数最小。书上的例子: 三个举证ABC,大小分别为  10x100,100x5 ,5x50

那么那种结合(AB)C 和 A(BC)的运算次数分别为 7500 ,25000 。那么如何给一个算法知道矩阵想乘的最小运算次数?

   输入为  vector<pair<int ,int>> a;   // pair 存放一个矩阵的行和列

   输出 为最小计算次数(也可以额外输出如何进行结合的s[i][j](表示i~j相乘中划分为 i~s[i][j] 与 s[i][j]+1~j两个最优数组时,它的运算次数最小)

动态规划的设计思路:

  如果暴力求解的话 时间复杂度为 2^n ,但是可以把为题划分为求两个最优解的和,划分成两部分。p[i][j] = p[i][k] + p[k+1][j] + a[i].first*a[j].second*a[k].second

  这里用一个二维数组存放计算的结果如下 。 p[i][j] 代表 A[i]*A[i+1]*....*A[j] 的最优运算次数。

计算次序为:  对号标记部分   小红旗标记部分  上升箭头标记部分  最后是所要求的结果。

  在kmp算法中也用到了这种思路的解法:

                               

程序接口及实现如下:

int maxtrix_chain(vector<pair<int, int >> a,int s[6][6]){
    int const len = a.size();
    int **p = new int*[len];
    for (int i = 0; i < len; i++){
        p[i] = new int[len];
    }
    for (int i = 0; i < len; i++){
        for (int j = i; j < len; j++){
            if (j == i + 1){
                s[i][j] = i;                                   //需要初始化的值
                p[i][j] = a[i].first*a[i].second*a[j].second;  //需要初始化的值
            }
            else
                p[i][j] = 0;
        }
    }
    for ( int k =  2; k <len; k++){
        for ( int i=0 ; i <len-k; i++){
            int re = INT_MAX;
            for (int j = 0; j < k ; j++){
                int temp=min(re,p[i][i+j] + p[i+j+1][k+i]+a[i].first*a[k+i].second*a[i+j].second);
                if (temp < re){
                    re = temp;
                    s[i][k + i] = i+j;
                }
            }
            p[i][k+i] = re;
        }
    }
    int resu = p[0][len - 1];
    for (int i = 0; i < len; i++)
        delete p[i];
    delete p;
    return resu;
}

根据s[6][6]输出结合方式的函数:

void print(int s[6][6],int  i, int j){
    if (i == j)
        cout << "A" << i;
    else{
        cout << "(";
        print(s, i, s[i][j]);
        print(s,s[i][j]+1,j);
        cout << ")";
    }
}

三、最长公共子序列(LCS)

1)描述一个最长公共子序列

  如果序列比较短,可以采用蛮力法枚举出X的所有子序列,然后检查是否是Y的子序列,并记录所发现的最长子序列。如果序列比较长,这种方法需要指数级时间,不切实际。

  LCS的最优子结构定理:设X={x1,x2,……,xm}和Y={y1,y2,……,yn}为两个序列,并设Z={z1、z2、……,zk}为X和Y的任意一个LCS,则:

(1)如果xm=yn,那么zk=xm=yn,而且Zk-1是Xm-1和Yn-1的一个LCS。

  (2)如果xm≠yn,那么zk≠xm蕴含Z是是Xm-1和Yn的一个LCS。

  (3)如果xm≠yn,那么zk≠yn蕴含Z是是Xm和Yn-1的一个LCS。

  定理说明两个序列的一个LCS也包含两个序列的前缀的一个LCS,即LCS问题具有最优子结构性质。

2)一个递归解

  根据LCS的子结构可知,要找序列X和Y的LCS,根据xm与yn是否相等进行判断的,如果xm=yn则产生一个子问题,否则产生两个子问题。设C[i,j]为序列Xi和Yj的一个LCS的长度。如果i=0或者j=0,即一个序列的长度为0,则LCS的长度为0。LCS问题的最优子结构的递归式如下所示:

3)计算LCS的长度

  采用动态规划自底向上计算解。书中给出了求解过程LCS_LENGTH,以两个序列为输入。将计算序列的长度保存到一个二维数组C[M][N]中,另外引入一个二维数组B[M][N]用来保存最优解的构造过程。M和N分别表示两个序列的长度。该过程的伪代码如下所示:

LCS_LENGTH(X,Y)
    m = length(X);
    n = length(Y);
    for i = 1 to m
      c[i][0] = 0;
    for j=1 to n
      c[0][j] = 0;
    for i=1 to m
       for j=1 to n
           if x[i] = y[j]
              then c[i][j] = c[i-1][j-1]+1;
                   b[i][j] = ‘\‘;           // 构造一个路径表,用于指示 。  \: 两边都可以
           else if c[i-1][j] >= c[i][j-1]
                  then c[i][j] = c[i-1][j];
                       b[i][j] = ‘|‘;       // | : 上下
                  else
                       c[i][j] = c[i][j-1];
                       b[i][j] = ‘-‘;      //  - : 左右
return c and b

四、[LeetCode] Interleaving String

Given s1s2s3, find whether s3 is formed by the interleaving of s1 and s2.

For example,
Given:
s1 = "aabcc",
s2 = "dbbca",

When s3 = "aadbbcbcac", return true.
When s3 = "aadbbbaccc", return false.

DP. 通过构造一个 二维数组 f[i][j] = (f[i-1]f[j] && s3[i+j-1]==s1[i])  || (f[i][j-1]&&s3[i+j-1]==s2[j])

class Solution {
private:
    bool f[1000][1000];
public:
    bool isInterleave(string s1, string s2, string s3) {
        // Start typing your C/C++ solution below
        // DO NOT write int main() function
        if (s1.size() + s2.size() != s3.size())
            return false;

        f[0][0] = true;
        for(int i = 1; i <= s1.size(); i++)
            f[i][0] = f[i-1][0] && (s3[i-1] == s1[i-1]);

        for(int j = 1; j <= s2.size(); j++)
            f[0][j] = f[0][j-1] && (s3[j-1] == s2[j-1]);

        for(int i = 1; i <= s1.size(); i++)
            for(int j = 1; j <= s2.size(); j++)
                f[i][j] = (f[i][j-1] && s2[j-1] == s3[i+j-1]) || (f[i-1][j] && s1[i-1] == s3[i+j-1]);

        return f[s1.size()][s2.size()];
    }
};

五、leetcode : unique bianry search tree 

  http://www.cnblogs.com/NeilZhang/p/5347667.html

总结:

  动态规划问题的难点是 找到 递推的关系,从而构造适当的数组。

时间: 2024-10-13 11:40:53

算法导论 第四部分——基本数据结构——第15章:动态规划的相关文章

MIT算法导论——第四讲.Quicksort

本栏目(Algorithms)下MIT算法导论专题是个人对网易公开课MIT算法导论的学习心得与笔记.所有内容均来自MIT公开课Introduction to Algorithms中Charles E. Leiserson和Erik Demaine老师的讲解.(http://v.163.com/special/opencourse/algorithms.html) 第四节-------快速排序 Quicksort 这节课的主要内容分为两部分,一部分是介绍快速排序算法,分析其在最好.最坏以及最好最差

算法导论学习(三)——数据结构

数据结构中字典(dictionary)的概念:支持在一个集合中插入和删除元素以及测试元素是否属于集合的操作的动态集合被称为字典. 动态集合假定对象中的一个属性被标识为关键字(key),对象可能包含卫星数据,它们与其他对象属性一起移动. 一.基本数据结构 1 栈和队列 栈(stack):后进先出(LIFO) 队列(queue):先进先出(FIFO) 栈顶指向最近被推入栈的元素的位置. 栈判空(时间复杂度O(1)): STACK-EMPTY(S) if S.top==0 return TRUE el

算法导论 第三部分——基本数据结构——红黑树

红黑树 红黑树是一种二叉查找树,但在每个结点上增加了一个存储位表示结点的颜色,可以是RED或者BLACK.通过对任何一条从根到叶子的路径上各个着色方式的限制, 红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的.当二叉查找树的高度较低时,这些操作执行的比较快,但是当树的高度较高时,这些操作的性能可能 不比用链表好.红黑树(red-black tree)是一种平衡的二叉查找树,它能保证在最坏情况下,基本的动态操作集合运行时间为O(lgn). 1.红黑树的性质 #define RED 0

算法导论第四版学习——习题三Collinear Points

题目正文: http://coursera.cs.princeton.edu/algs4/assignments/collinear.html 作业难点: 1.仔细思考会感觉有很多实现方法,但是如果没有适当使用排序,算法时间复杂度就会轻易超过要求.(包括暴力算法) 2.隐含需要实现自己的数据结构用来组织“线”的集合,当然也可以用Stack或者Queue或者LinkedList,但是我个人是自己实现的. 3.由于一条线段只能出现一次,所以有一个去重的问题.(最难) 作业技巧: 1.基本按照题目的先

算法导论第四章分治策略编程实践(二)

在上一篇中,通过一个求连续子数组的最大和的例子讲解,想必我们已经大概了然了分治策略和递归式的含义,可能会比较模糊,知道但不能用语言清晰地描述出来.但没关系,我相信通过这篇博文,我们会比较清楚且容易地用自己的话来描述. 通过前面两章的学习,我们已经接触了两个例子:归并排序和子数组最大和.这两个例子都用到了分治策略,通过分析,我们可以得出分治策略的思想:顾名思义,分治是将一个原始问题分解成多个子问题,而子问题的形式和原问题一样,只是规模更小而已,通过子问题的求解,原问题也就自然出来了.总结一下,大致

算法导论第四版学习——习题一Percolation

题目正文: http://coursera.cs.princeton.edu/algs4/assignments/percolation.html 作业难点: 1.backwash(倒灌)的判断,如果不使用另一个WeightedUnionFind对象,要在要求的时间和空间范围内实现是很困难的 和论坛里的学生一样,尝试只只使用上部的虚拟节点,下部判断联通使用循环+break,提交后不满足时间复杂度要求 你也可以不考虑backwash这种情况,因为backwash对连通性并没有直接影响 2.Unio

算法导论第四版学习——习题五Kd-Tree

题目正文: http://coursera.cs.princeton.edu/algs4/assignments/kdtree.html 作业难点: 如何组织自己的数据结构是本道题的最难点,基本上有思路就肯定可以完成.题目一定要看仔细,基本上2dtree部分已经把实现原理说清楚了. 作业技巧: 1.逐步实现,实现完成后用insert.contain测试下,没问题再写draw,测试没问题再写其余的. 2.重复输入的问题不要忘了 3.range虽然api里写的是all point inside th

算法导论第四版学习——习题二Deques and Randomized Queues

题目正文: http://coursera.cs.princeton.edu/algs4/assignments/queues.html 作业难点: 1.选择使用链表还是数组,将会是第一个问题,选择合适会减少很多工作量. 2.并行从RandomizedQueue中取两个枚举,这两个枚举必须是不一样的,所以有很多偷懒的“伪随机”是行不通的. 3.SubSet仅需K存储而不是N存储,即可实现.(这个我也没想到方法实现) 作业技巧: 1.可遍数组和邻接节点两种数据结构都已经给你了,你只要改改,基本上实

算法导论第四版学习——习题四8 Puzzle

题目正文: http://coursera.cs.princeton.edu/algs4/assignments/8puzzle.html 作业难点: 1.如何验证Puzzle是否可解?题目中有提示,如果相邻两个格子交换后得到的“Twin Puzzle”进行求解,如果Twin有解那么原始Puzzle就无解. 作业技巧: 1.checklist提到把Twin Puzzle和Puzzle放在一个Priority Queue中,其实就是在设计自己的Node数据结构的时候加一个boolean类型标识是否