- 概述
- 基本思想
基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。
如果问题是由交叠的子问题所构成,我们就可以用动态规划技术来解决它,一般来说,这样的子问题出现在对给定问题求解的递推关系中,这个递推关系包含了相
同问题的更小子问题的解。动态规划法建议,与其对交叠子问题一次又一次的求解,不如把每个较小子问题只求解一次并把结果记录在表中(动态规划也是空间换时间
的),这样就可以从表中得到原始问题的解。
我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
- 适用条件
能采用动态规划求解的问题的一般要具有3个性质:
1.最优化原理(最优子结构性质): 如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。最优化原理可这样阐述:不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。
2.无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
3.子问题的重叠性:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)。和分治算法比较类似,但不同的是分治算法把原问题划归为几个相互独立的子问题,从而一一解决,而动态规划则是针对子问题有重叠的情况的一种解决方案。
动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
- 基本步骤
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。一般要经历以下几个步骤。
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
实际应用中可以按以下几个简化的步骤进行设计
(
1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
使用动态规划求解问题,最重要的就是确定动态规划三要素:
(1)问题的阶段 (2)每个阶段的状态 (3)从前一个阶段转化到后一个阶段之间的递推关系。
递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。
确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}
一个动态规划算法的几个关键点:
1)怎么描述问题,要把问题描述为交叠的子问题
2)交叠子问题的初始条件(边界条件)
3)动态规划在形式上往往表现为填矩阵的形式(在后面会看到,有的可以优化空间复杂度,开一个数组即可,优化也是根据递推式的依赖形式的,后面有篇文章详细说明)
4)填矩阵的方式(或者说顺序)表明了什么?--它表明了这个动态规划从小到大产生的过程,专业点的说就是递推式的依赖形式决定了填矩阵的顺序。
个人体会是动态规划的难点在于前期的设计:
a)怎么描述问题,使它能表述为一个动态规划问题(具备什么特征?最有子结构,多阶段决策,思考)
b)递推式的写出(逆向思维去分析或正向思维去递归),确定你要求的是哪个值
c)有了递推式可以画个矩阵的图(一般只从式子上不太容易看出来,当然,对于牛人来说可以藐视),在图中关注以下两点:
初始条件
填矩阵的顺序(即怎么去写代码控制语句)
有了这些之后,其实动态规划的代码都很简单,它的难点在于问题的描述和解决阶段,而不在于写代码的阶段,剩下的写代码基本上就是照着公式填矩阵。
目前design DP主要有两个思路:
一个是利用recursive method,即首先把问题用递归的方法解决,然后用一个table保存recursive中的中间结果,这不就避免了递归中重复计算的低效了吗?遇到需要计算以前计算过的东西,直接查表就OK,总之一句话,先写recursive,然后比葫芦画瓢基本就能把DP的方法写出来。这里的难点是如何找到recursive。算法导论里面也给的是这个思路。下面的前三个例子全部出自《算法导论》。
另一个思路是exhaust search,这个好像是我们老师发明的方法,这里有篇Kirk的论文, How to design dynamic programming algorithms sans recursion 有兴趣的大家可以仔细研究一下,我下面也会简单举例介绍一下这个方法。
下面的例子中多数代码都是伪代码,旨在illustrate idea。同时节省时间。代码中都省去了backtrack的过程,即只得到了optimal solution的值,省去了如何construct optimal solution的过程。这个一般用一个数组记录一下就OK了。
- 示例
1.计算二项式系数:
参考:
http://www.cnblogs.com/kkgreen/archive/2011/06/26/2090702.html
在排列组合里面,我们有下面的式子(很容易用组合的定义来证明):
这个式子将C(n , k)的计算问题表述为了(问题描述)C(n-1 , k -1)和C(n -1, k)两个较小的交叠子问题。
初始条件:C(n , n) = C(n , 0) = 1
我们可以用下列填矩阵的方式求出C(n , k):
该算法的时间复杂度是多少呢?可以大概的估计下,只填了下三角矩阵,为n*k/2 = n*k,具体的次数为:
按行来填矩阵:算法伪代码:
第1个for是控制行的,要填到第n行。第2个for来控制每行填到哪的,到i和k的较小值。从这2个for也可以看出复杂度是n*k。
package Section8;
/*第八章 动态规划 计算二项式系数*/
publicclass BinoCoeff {
/**
* @param args
*/
publicstaticvoid main(String[] args) {
// TODO Auto-generated method stub
int result = Binomial(8,3);
System.out.println("输出8的二项式系数:");
for(int i =0;i <=8;i++)
System.out.println("C"+"("+8+","+ i +")"+" ———— "+ Binomial(8,i));
}
publicstaticint Binomial(int n,int k){
//计算二项式系数C(n,k)
int[][] result =newint[n+1][n+1];
for(int i =0;i <= n;i++) //按行来填矩阵
{
for(int j =0;j <= min(i,k);j++) // min(i,k)是这一行需要填的列数
{
//if(j == 0 || j == k)//书上写错了
if(j ==0|| j == i)
result[i][j] =1;
else
result[i][j] = result[i -1][j -1] + result[i -1][j];
}
}
return result[n][k];
}
privatestaticint min(int i,int k){
if(i < k)
return i;
return k;
}
}
如何优化空间复杂度:
——为什么可以优化,上面说过,可不可以优化,以及如何优化空间复杂度依赖于它的递推形式:
——从填矩阵的那张图可以看出,这个动态规划产生各项的过程(如果按行填的话)是上一行的第 i-1 项和第 i 项加起来产生下一行的第 i 项,传统上,我们从左往右填。
——事实上,根据它的产生过程(这个产生过程依赖于递推式自身的数学特征),可以从右往左填,这样开一个数组就行,在原数组上本地不动的填数,从右往左填可以保证一个位置在覆盖以后不会再被用到(这是由递推式的属性决定的,需要画一画才看的比较清楚)。这样开一个K大的数组就行了,具体的实现就不写了,已经分析的很清楚了,实现也不难
2.国际象棋最短路径问题
参考:
http://www.cnblogs.com/kkgreen/archive/2011/06/26/2090702.html
国际象棋中的车可以水平的或竖直的移动,一个车要从一个棋盘的一角移到对角线的另一角,有多少种最短路径?
a,用动态规划算法求解
b,用初等排列组合知识求解
a)主要是培养下怎么去建立动态规划的递推式
问题是从(0,0)移动到(n,n)有多少种方法?(最短路,即横n竖n,不能回退)
设C[i , j]表示从(0,0)移动到(i ,j)的方法数(描述问题,怎么去刻画C[i , j]的含义,是动态规划的一个关键点):
那么怎么才能走到(i ,j)呢,它的上一步必定是(i-1 ,j)或者(i ,j-1)-------(分析动态规划问题的逆向思维,很重要,后面要讲)这样就将问题描述为了交叠子问题:
C[i , j] = C[i -1, j] + C[i , j-1] ( C[i , j]的含义 )
我们要求的是C[n , n]
初始条件:
C[0 , j] = j j从0到n
C[i , 0] = i i从0到n
即第一行第一列确定。
填矩阵的形式:可以按行也可以按列。以上分析画个图很容易看出来。剩下的实现就很简单了。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。
同问题的更小子问题的解。动态规划法建议,与其对交叠子问题一次又一次的求解,不如把每个较小子问题只求解一次并把结果记录在表中(动态规划也是空间换时间
的),这样就可以从表中得到原始问题的解。
- 适用条件
(4)根据计算最优值时得到的信息,构造问题的最优解
使用动态规划求解问题,最重要的就是确定动态规划三要素:
(1)问题的阶段 (2)每个阶段的状态 (3)从前一个阶段转化到后一个阶段之间的递推关系。
递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。
确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}
一个动态规划算法的几个关键点:
1)怎么描述问题,要把问题描述为交叠的子问题
2)交叠子问题的初始条件(边界条件)
3)动态规划在形式上往往表现为填矩阵的形式(在后面会看到,有的可以优化空间复杂度,开一个数组即可,优化也是根据递推式的依赖形式的,后面有篇文章详细说明)
4)填矩阵的方式(或者说顺序)表明了什么?--它表明了这个动态规划从小到大产生的过程,专业点的说就是递推式的依赖形式决定了填矩阵的顺序。
个人体会是动态规划的难点在于前期的设计:
a)怎么描述问题,使它能表述为一个动态规划问题(具备什么特征?最有子结构,多阶段决策,思考)
b)递推式的写出(逆向思维去分析或正向思维去递归),确定你要求的是哪个值
c)有了递推式可以画个矩阵的图(一般只从式子上不太容易看出来,当然,对于牛人来说可以藐视),在图中关注以下两点:
初始条件
填矩阵的顺序(即怎么去写代码控制语句)
有了这些之后,其实动态规划的代码都很简单,它的难点在于问题的描述和解决阶段,而不在于写代码的阶段,剩下的写代码基本上就是照着公式填矩阵。
目前design DP主要有两个思路:
一个是利用recursive method,即首先把问题用递归的方法解决,然后用一个table保存recursive中的中间结果,这不就避免了递归中重复计算的低效了吗?遇到需要计算以前计算过的东西,直接查表就OK,总之一句话,先写recursive,然后比葫芦画瓢基本就能把DP的方法写出来。这里的难点是如何找到recursive。算法导论里面也给的是这个思路。下面的前三个例子全部出自《算法导论》。
另一个思路是exhaust search,这个好像是我们老师发明的方法,这里有篇Kirk的论文, How to design dynamic programming algorithms sans recursion 有兴趣的大家可以仔细研究一下,我下面也会简单举例介绍一下这个方法。
下面的例子中多数代码都是伪代码,旨在illustrate idea。同时节省时间。代码中都省去了backtrack的过程,即只得到了optimal solution的值,省去了如何construct optimal solution的过程。这个一般用一个数组记录一下就OK了。
- 示例
1.计算二项式系数:
参考:
http://www.cnblogs.com/kkgreen/archive/2011/06/26/2090702.html
在排列组合里面,我们有下面的式子(很容易用组合的定义来证明):
这个式子将C(n , k)的计算问题表述为了(问题描述)C(n-1 , k -1)和C(n -1, k)两个较小的交叠子问题。
初始条件:C(n , n) = C(n , 0) = 1
我们可以用下列填矩阵的方式求出C(n , k):
该算法的时间复杂度是多少呢?可以大概的估计下,只填了下三角矩阵,为n*k/2 = n*k,具体的次数为:
按行来填矩阵:算法伪代码:
第1个for是控制行的,要填到第n行。第2个for来控制每行填到哪的,到i和k的较小值。从这2个for也可以看出复杂度是n*k。
package Section8;
/*第八章 动态规划 计算二项式系数*/
publicclass BinoCoeff {
/**
* @param args
*/
publicstaticvoid main(String[] args) {
// TODO Auto-generated method stub
int result = Binomial(8,3);
System.out.println("输出8的二项式系数:");
for(int i =0;i <=8;i++)
System.out.println("C"+"("+8+","+ i +")"+" ———— "+ Binomial(8,i));
}
publicstaticint Binomial(int n,int k){
//计算二项式系数C(n,k)
int[][] result =newint[n+1][n+1];
for(int i =0;i <= n;i++) //按行来填矩阵
{
for(int j =0;j <= min(i,k);j++) // min(i,k)是这一行需要填的列数
{
//if(j == 0 || j == k)//书上写错了
if(j ==0|| j == i)
result[i][j] =1;
else
result[i][j] = result[i -1][j -1] + result[i -1][j];
}
}
return result[n][k];
}
privatestaticint min(int i,int k){
if(i < k)
return i;
return k;
}
}
如何优化空间复杂度:
——为什么可以优化,上面说过,可不可以优化,以及如何优化空间复杂度依赖于它的递推形式:
——从填矩阵的那张图可以看出,这个动态规划产生各项的过程(如果按行填的话)是上一行的第 i-1 项和第 i 项加起来产生下一行的第 i 项,传统上,我们从左往右填。
——事实上,根据它的产生过程(这个产生过程依赖于递推式自身的数学特征),可以从右往左填,这样开一个数组就行,在原数组上本地不动的填数,从右往左填可以保证一个位置在覆盖以后不会再被用到(这是由递推式的属性决定的,需要画一画才看的比较清楚)。这样开一个K大的数组就行了,具体的实现就不写了,已经分析的很清楚了,实现也不难
2.国际象棋最短路径问题
参考:
http://www.cnblogs.com/kkgreen/archive/2011/06/26/2090702.html
国际象棋中的车可以水平的或竖直的移动,一个车要从一个棋盘的一角移到对角线的另一角,有多少种最短路径?
a,用动态规划算法求解
b,用初等排列组合知识求解
a)主要是培养下怎么去建立动态规划的递推式
问题是从(0,0)移动到(n,n)有多少种方法?(最短路,即横n竖n,不能回退)
设C[i , j]表示从(0,0)移动到(i ,j)的方法数(描述问题,怎么去刻画C[i , j]的含义,是动态规划的一个关键点):
那么怎么才能走到(i ,j)呢,它的上一步必定是(i-1 ,j)或者(i ,j-1)-------(分析动态规划问题的逆向思维,很重要,后面要讲)这样就将问题描述为了交叠子问题:
C[i , j] = C[i -1, j] + C[i , j-1] ( C[i , j]的含义 )
我们要求的是C[n , n]
初始条件:
C[0 , j] = j j从0到n
C[i , 0] = i i从0到n
即第一行第一列确定。
填矩阵的形式:可以按行也可以按列。以上分析画个图很容易看出来。剩下的实现就很简单了。
有了递推式后,发现其实跟上一题完全一样,就是递推式里多乘了个概率值。
我们要求的是P[i , j]
初始条件:
C[0 , j] = q^j j从0到n
C[i , 0] = p^i i从0到n
即第一行第一列确定。
填矩阵的形式:可以按行也可以按列。以上分析画个图很容易看出来。剩下的实现就很简单了。
4.0/1背包问题
01背包: 有N件物品和一个重量为M的背包。(每种物品均只有一件)第i件物品的重量是w[i],价值是p[i]。求解将哪些物品装入背包可使价值总和最大。
完全背包: 有N种物品和一个重量为M的背包,每种物品都有无限件可用。第i种物品的重量是w[i],价值是p[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包重量,且价值总和最大。
多重背包: 有N种物品和一个重量为M的背包。第i种物品最多有n[i]件可用,每件重量是w[i],价值是p[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包重量,且价值总和最大。
动态规划(DP)求解:
1) 子问题定义:F[i][j]表示前i件物品中选取若干件物品放入剩余空间为j的背包中所能得到的最大价值。
2) 根据第i件物品放或不放进行决策
(1-1)
其中F[i-1][j]表示前i-1件物品中选取若干件物品放入剩余空间为j的背包中所能得到的最大价值;
而F[i-1][j-C[i]]+W[i]表示前i-1件物品中选取若干件物品放入剩余空间为j-C[i]的背包中所能取得的最大价值加上第i件物品的价值。
根据第i件物品放或是不放确定遍历到第i件物品时的状态F[i][j]。
设物品件数为N,背包容量为V,第i件物品体积为C[i],第i件物品价值为W[i]。
#include<iostream>
#define max(a,b) ((a) > (b) ? a : b)
int
c[5] = {3,5,2,7,4};
int
v[5] = {2,4,1,6,5};
int
f[6][10] = {0};
//f[i][v] = max{ f[i-1][v] , f[i-1][v - c[i]] + w[i]}