您的位置:首页 > 编程语言

算法设计思想之动态规划

2017-02-09 16:32 218 查看
学习编程也有一定时间了,对于算法也有了一些了解。逐渐发现很多算法的思想都是一样的,那就是动态规划,于是以自己的理解写下本篇文章作为动态规划的一个入门,我会尽可能用直白的语言深入浅出的解释动态规划。由于本人水平有限,不免会出现不足之处甚至错误,请各位多包涵。此文章将会同步发表在本人csdn博客上,欢迎前来观看。
        动态规划,英文名称dynamic programming,是运筹学的一个分支,是求解决策过程(decision
process)最优化的数学方法。在编程以及算法设计中随处可见它的身影,这里需要注意,动态规划指的并不是一种具体的算法,而是一类算法,更准确的说是一类算法中蕴含的数学思想。动态规划算法应用十分广泛,毫不夸张的说,几乎所有的最优化问题都可以用动态规划算法解决,而且时间空间复杂度都十分理想,而最优化问题又是编程中十分常见的一大类问题。所以说,要成为一个合格的程序员,必须掌握动态规划。
       依据本人的理解,动态规划的核心思想主要有两个方面。第一便是问题分解,将某个大问题(这里问题的大小依据输入规模确定)分解为若干个容易求解的子问题,分别求解这些子问题,再合并子问题的解,从而得到原问题的解。如果子问题规模仍然比较大,可以进一步分解,直到分解为该问题的基本情况,再重复合并步骤。问题基本情况的解一般直接可以得到。

       动态规划的第二个核心思想便是保存子问题的解,在分别求解子问题以及子子问题的过程中,很有可能需要多次重复求解相同的字问题。如果不保存子问题的解,便浪费大量时间重复求解相同的子问题。因此动态规划采用某种形式保存子问题的解,在求解某一子问题时,先访问结果集,如果该子问题已求解,则直接返回结果,需要常数时间。若未求解,则求解该子问题,并且把结果保存到结果集中,这样可以大大节省时间。

       动态规划的基本使用形式有两种,分别是自顶向下的递归方法以及自底向上的方法。自顶向下方法从原问题开始,首先分解原问题为若干子问题,分别求解子问题后再合并,一般需要用到递归。自底向上从最小的,不可再分的子问题开始,一步步合并子问题为更大的子问题,从而得到原问题的解,一般不需要用到递归。

       下面以两个十分经典的例子阐明动态规划的使用。

       1(最大子数组和问题).问题描述:给出一个数组a,长度为n。对于任意0<=i<=j<=n-1,所有满足i<=k<=j的a[k]组成的集合为a的一个子数组(subarray),子数组中各个元素的和称为子数组的和,求a所有子数组和中最大的一个。(子数组中最少包含一个元素)

       看到这个问题,我们的第一反应便是暴力搜索,枚举a的所有子数组,求其和,选出最大的一个。这个算法需要三层嵌套循环,时间复杂度为o(n³),代码略。

      那么有没有好一点的方法呢?一般最优化问题暴力枚举都不是最佳方法。我们继续思考,分治法怎么样?可以把a分为两部分,那么和最大的子数组一共有三种情况,第一,子数组完全在第一部分中;第二,子数组完全在第二部分中;第三,子数组的前半部分在第一部分中,后半部分在第二部分中。对于前两种情况,可以递归求解,对于第三种情况,我们设原数组的切分点在下标k,k+1之间,由于子数组是连续的,所以前半部分以a[k]结尾的子数组的最大和加上后半部分以a[k+1]开头的子数组的最大和加起来就是第三种情况中的最大和,三者比较,取最大值,就是结果。而前半部分与后半部分的最大和均可以在线性时间内求出。注意,这里需要满足子数组中最少有一个元素的条件,故需要加一个判断。总的时间复杂度是多少呢?设输入规模为n的时候所需时间为T(n),平均划分的话,递归求解两个子问题需要2T(n/2),比较需要o(1),故有T(n)
= 2T(n/2)+o(n)+o(1),解得T(n)=o(nlgn),时间复杂度已经有了很大改善。
     上文说过,几乎所有最优化问题都可以用动态规划来解决,我们不妨考虑一下它。动态规划的核心是什么?分解子问题!对,我们就来尝试分解一下。首先来把所有子数组按照结尾元素分类,每一个子数组的结尾元素从a[0]到a[n-1],各不相同。如果我们可以求得以a[k]结尾的所有子数组的最大和,那么所有这些最大和里面的最大值,不就是答案吗? 可是如何求这些最大和呢?

     我们用b[k]表示以a[k]结尾的子数组的最大和。
     如果已知b[k],那么我们能不能得到b[k+1]呢?

     答案是肯定的。以a[k+1]结尾的子数组,必然包括a[k+1],可能包括,也可能不包括a[0]到a[k]中的元素。如果以a[k+1]结尾的和最大的子数组只包括a[k+1],那么和就是a[k+1],如果还包括a[0]到a[k]中的元素,那么最大和就是a[k+1]+b[k](想想为什么,利用反证法很容易证明,此处证明略)那么a[k+1]与a[k+1]+b[k]哪个大呢?这取决于b[k]是否大于0.。我们据此列出状态转移方程:b[k+1] = a[0](k=0||b[k]<0);b[k+1] = b[k]+a[k+1](b[k]>0)。状态转移方程一旦列出来,代码就呼之欲出了,下面上代码:

 

int maxSubarraySum(int * a,int r) {
    int i,
    temp=0,
    summax=INT_MIN;
    for(i=l;i<=r;i++){
        temp+=a[i];
        if(temp > summax) summax=temp;
        if(temp < 0) temp=0;
    }
    return summax;
}

 
这道题是利用自底向上方法进行求解的,从0一步步推到n-1,一个个合并子问题。只需要保存上一个元素的结果,因此时间复杂度与空间复杂度都十分优秀,时间复杂度为o(n),空间复杂度为o(1)。下面来看第二个问题:

 2(01背包问题)问题描述:假设现有容量10kg的背包,另外有3个物品,分别为a1,a2,a3。物品a1重量为3kg,价值为4;物品a2重量为4kg,价值为5;物品a3重量为5kg,价值为6。将哪些物品放入背包可使得背包中的总价值最大?

先将原始问题一般化,欲求背包能够获得的总价值,即欲求前i个物体放入容量为m(kg)背包的最大价值c[i][m]——使用一个数组来存储最大价值,当m取10,i取3时,即原始问题了。而前i个物体放入容量为m(kg)的背包,又可以转化成前(i-1)个物体放入背包的问题。下面使用数学表达式描述它们两者之间的具体关系。

  表达式中各个符号的具体含义。

  w[i] :  第i个物体的重量;

  p[i] : 第i个物体的价值;

  c[i][m] : 前i个物体放入容量为m的背包的最大价值;

  c[i-1][m] : 前i-1个物体放入容量为m的背包的最大价值;

  c[i-1][m-w[i]] : 前i-1个物体放入容量为m-w[i]的背包的最大价值;

  由此可得:
      c[i][m]=max{c[i-1][m-w[i]]+pi , c[i-1][m]}(下图将给出更具体的解释)

根据上式,对物体个数及背包重量进行递推,列出一个表格(见下表),表格来自(http://blog.csdn.net/fg2006/article/details/6766384?reload) ,当逐步推出表中每个值的大小,那个最大价值就求出来了。推导过程中,注意一点,最好逐行而非逐列开始推导,先从编号为1的那一行,推出所有c[1][m]的值,再推编号为2的那行c[2][m]的大小。这样便于理解。



下面上代码:

 

#include <stdio.h>
int c[10][100]={0};

void knap(int m,int n){

    int i,j,w[10],p[10];
    for(i=1;i<n+1;i++)
        scanf("%d,%d",&w[i],&p[i]);
    for(j=0;j<m+1;j++)
        for(i=0;i<n+1;i++)
    {
        if(j<w[i])
        {
            c[i][j]=c[i-1][j];
            continue;
        }else if(c[i-1][j-w[i]]+
a293
p[i]>c[i-1][j])
            c[i][j]=c[i-1][j-w[i]]+p[i];
        else
            c[i][j]=c[i-1][j];
    }
    
}            

 

这个问题是二维的动态规划,而且需要保存每一个子问题的结果,与上题稍有不同。

动态规划是一门非常有用的算法,上述两个仅仅是动态规划最经典也是最基础的应用,要掌握动态规划,还有很长的一段路要走。

(纯属原创,转载请注明出处) 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息