动态规划,英文名称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)。状态转移方程一旦列出来,代码就呼之欲出了,下面上代码:
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]的大小。这样便于理解。
下面上代码:
这个问题是二维的动态规划,而且需要保存每一个子问题的结果,与上题稍有不同。
动态规划是一门非常有用的算法,上述两个仅仅是动态规划最经典也是最基础的应用,要掌握动态规划,还有很长的一段路要走。
(纯属原创,转载请注明出处)