算法设计思想之动态规划

学习编程也有一定时间了,对于算法也有了一些了解。逐渐发现很多算法的思想都是一样的,那就是动态规划,于是以自己的理解写下本篇文章作为动态规划的一个入门,我会尽可能用直白的语言深入浅出的解释动态规划。由于本人水平有限,不免会出现不足之处甚至错误,请各位多包涵。此文章将会同步发表在本人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 ]] + 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 ] ;
     }
    
}            
 

这个问题是二维的动态规划,而且需要保存每一个子问题的结果,与上题稍有不同。
动态规划是一门非常有用的算法,上述两个仅仅是动态规划最经典也是最基础的应用,要掌握动态规划,还有很长的一段路要走。
(纯属原创,转载请注明出处) 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值