算法导论 第四部分——基本数据结构——第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

总结:

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

 

posted @ 2016-07-12 21:47  NeilZhang  阅读(979)  评论(0编辑  收藏  举报