【算法导论】读书笔记——动态规划

一. 动态规划介绍

动态规划与分治法整体思路相近:组合子问题的解求解原问题。将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。

动态规划的应用场景:子问题重叠。子问题的解决需要解决子子问题(递归),而不同子问题之间的子子问题是相同的。

动态规划的实现及特点:对于分治算法而言,它只是把问题分解为几个子问题,再分别求解这些子问题,而在这些子问题的解决中,会把它分解为子子问题,解决每一个子子问题,由于这些子子问题很多都是重复的,因而每次都重新计算会浪费计算资源。而对于动态规划而言,它对于每个子子问题只求解一次,将问题的解保存下来,在再一次遇见这些子子问题的时候,就可以直接调用这些子子问题的解,而不用重新计算。故动态规划能通过保存中间子子问题的解来降低算法复杂度,而额外的空间往往只需要一个表格/数组就可以。

动态规划应用的常见问题:最优化问题,对某个场景寻求最优解(最大/最小值)。

二.动态规划算法

步骤:

1.刻画一个最优解的结构特征

2.递归地定义最优解的值

3.计算最优解的值,

4.利用计算出的信息构造一个最优解

三.举例

1.钢条切割

问题介绍:

假定我们知道一家公司出售一段长度为i 的钢条的价格为pi(i=1,2,...),钢条的长度为整数,下图是价格表

长度i12345678910
价格pi1589101717202430

钢条切割问题:给定一段长度为n的钢条和一个价格表pi(i=1,2,...n),求切割钢条方案,使得销售收益Rn最大,若长度为n的钢条的pn足够大,最优解可能完全不需要切割。

问题分析:

假设有n=4,有三个切割位置1-2,2-3,3-4,以1,0编码它们是否切割,得到以下切割方案,对应于0-7的二进制码,有以下8种切割方案:

A. 4 (不切割);B. 3 1;C. 2 2;D. 2 1 1;

E. 1 3 ;F. 1 2 1 ;G. 1 1 2;H. 1 1 1 1;

很显然,最有解应当是C,R=5+5=10。

对于长度为n的钢条,它的切割位置有n-1个,可以选择切割或者不切割,故它的切割方案有2^(n-1)。

动态规划算法应用分析:

第一步,刻画一个最优解的结构特征。

也就是说我们要先假设已经得到了最优解,那么针对这道题,我们已经得到了分割后的几段钢条,得到了几个最优切割位置,那么我们可以考虑选择一个最优切割位置切割,然后得到两个子钢条(每个子钢条不止一段),而每个子钢条都是经最优切割位置切割好的。而每个子钢条就相当于原钢条,最后把每个切割位置都切割后,问题就解决了。所以我们的最优解的结构特征就是:每个子钢条都是经最优切割位置切割好的。

好我们重头再来一遍,这次我们要加上具体的实现操作,我们选择一个最优切割位置切割,那么如何选择一个最优切割位置切割呢,最简单的方法,就是把每个位置都切割一遍,看看哪个最优。所以我们有公式

r_{n}=max(p_{n},r_{1}+r_{n-1},r_{2}+r_{n-2},...,r_{n-1}+r_{1})

注:pn就表示不切割的情况,共有n-1个切割位置。

在计算中,这个递归表达式的终止条件是n=1,此时r1=p1

实际上相比于每次将一个原问题分为两个问题,还有一种更简单的方式,就是我们每次切割的时候,保证切割的其中一条子钢条只有一段,而另一条子钢条是经最优切割位置切割好的。,而不仅仅是切为两个子钢条。于是公式可以简化为

r_{n}=\max_{1\leq i\leq n}(p_{i}+r_{n-i})

这样原问题的最优解只包含一个相关子问题,而不是两个。

直接递归 C++代码

/*p[0]保存p1值*/
cut_rod(p,n){
    if(n==0)
        return 0;
    q=-1;
    for(int i=1;i<=n;i++)
    {
        q=max(q,p[i-1]+cut_rod(p,n-i)); /*在i位置切割*/
    }
    return q;
}

但这样做是不够的,我们注意到,虽然我们是将一个原问题分解为一个子问题,但是在for循环中,每个子问题都要计算一次,长度为n的问题要调用n个长度分别为0,1,2,...,n-1的子问题,递归次数

n_{i}=1+\sum_{j=0}^{j=i-1} n_{i-1}   第一项1表示自身被调用。

当i=4时,有

n4=1+n3+n2+n1+n0=1+(1+n2+n1+n0)+n2+n1+n0=2(1+n2+n1+n0)=2(1+(1+n1+n0)+n1+n0)=4(1+n1+n0)=4(1+(1+n0+n0)=8(n0+1)=16

于是总共调用次数为2^n-1,它的复杂度为O(2^n),是无法求解的。

于是,我们来思考切割钢条的子子问题:子钢条到底有几种呢?仅有n种长度的子钢条,也就是说在递归求解子问题的时候一直在重复计算这些子子问题,这才使得运算效率变得极低,事实上只需要把这n种情况全部计算一遍,就可以得到最终的解。

动态规划算法,就是计算这些子子问题的解,再组合这些子子问题的解,得到最优解。

主要有两种实现方式,第一种是自顶而下的方式,在上面所讲的递归方式中加入备忘机制,使得每个子子问题在计算了一次之后记录下来,第二次遇见的时候就可以直接调用

自顶而下 C++代码

/*p[0]保存p1值*/
memoized_cut_rod(int *p,int n){
    int *r=(int*)malloc((n+1)*sizeof(int));
    for(int i=0;i<n;i++)
        r[i]=-1;
    return cut_rod(p,n,r)
}
memoized_cut_rod_aux(int *p,int n,int *&n){
    if(r[n]>=0)  /*正数表示该子子问题已计算*/
        return r[n]
    if(n==0)
        return 0;
    q=-1;
    for(int i=0;i<n;i++)
    {
        q=max(q,p[i]+memoized_cut_rod_aux(p,n-i-1));
    }
    r[n]=q;  /*记录子子问题*/
    return q;
}

将辅助r数组作为子子问题的结果存储,一旦遇见相同问题,就可以直接调用,而不用重复求解。

第二种方法是自底而上,既然原问题可以通子子问题的组合求解,那么把子子问题由简单到复杂逐步求解也能实现。

自底而上 C++代码

/*p[0]保存p1值*/
bottom_up_cut_rod(int *p,int n){
    int *r=(int*)malloc((n+1)*sizeof(int));
    r[0]=0;
    for(int j=1;j<=n;j++)
    {
        q=-1;
        for(int i=1;i<=j;i++)
        {
            q=max(q,p[i-1]+r[j-i]);
        }
        r[j]=q;
    }
    return r[n];
}

自底而上是将子子问题,即子钢条长度按从小到大依次求解,计算i规模的子问题依赖比i更小规模的子问题。

拓展:如果问题再变得复杂一点,钢条的售价不仅仅依赖于它的长度,而且还依赖于它的位置,也即p(i,j),i,j分别表示初始结束位置,切割位置依旧只能选择整数位置。

那么对于这个新问题,它的子问题依旧是切割一次后的子钢条,而它的子子问题就变成了每一种可能的子钢条情况,不再是简单的长度从1到n,而是要首尾各排列组合,也就是n(n+1)/2种情况,在这个时候,我们在使用动态规划的时候,就需要用一个二维数组来储存这n(n+1)/2个数值。

 

 

四. 参考文献

1.《算法导论》

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值