经典算法动态规划教程.docx
《经典算法动态规划教程.docx》由会员分享,可在线阅读,更多相关《经典算法动态规划教程.docx(95页珍藏版)》请在冰点文库上搜索。
经典算法动态规划教程
也互不相同,因而动态规划的没计法对不同的问题,有各具特色的表示方式。
不存在一种万能的动态规
划算法。
但是可以通过对若干有代表性的问题的动态规划算法进行讨论,学会这一设计方法。
多阶段决策过程最优化问题
――动态规划的基本模型
在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。
因此各个阶段决策的选取不能任意确定,它依赖于当前面临的状态,又影响以后的发展。
当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线。
这种把一个问题看做是一个前后关联具有链状结构的多阶段
过程就称为多阶段决策过程,这种问题称为多阶段决策最优化问题。
【例题1】最短路径问题。
图中给出了一个地图,地图中每个顶点代表一个城市,两个城市间的连线代表道路,连线上的数值代表道路的长度。
现在,想从城市A到达城市E,怎样走路程最短,最短
路程的长度是多少?
【分析】把从A到E的全过程分成四个阶段,用k表示阶段变量,第1阶段有一个初始状态A,
两条可供选择的支路ABl、有两条可供选择的支路••…路径距离,Fk(Xk)表示从第具体计算过程如下:
AB2;第2阶段有两个初始状态B1、B2,B1有三条可供选择的支路,B2•。
用dk(Xk,Xk+1)表示在第k阶段由初始状态xk到下阶段的初始状态Xk+1的k阶段的xk到终点E的最短距离,利用倒推方法求解A到E的最短距离。
S1:
K=4,有:
F4(D1)=3,F4(D2)=4,F4(D3)=3
S2:
K=3,有:
F3(C1)=min{d3(C1,D1)+F4(D1),d3(C1,D2)+F4(d2)}=min{8,10}=8
F3(C2)=d3(C2,D1)+f4(D1)=5+3=8
F3(C3)=d3(C3,D3)+f4(D3)=8+3=11
F3(C4)=d3(C4,D3)+f4(D3)=3+3=6
S2:
K=2,有:
F2(B1)=min{d2(B1,C1)+F3(C1),d2(B1,C2)+f3(C2),d2(B1,C3)+F3(C3)}=min{9,12,14}=9
F2(m)=min{d2(B2,c2)+f3(C2),d2(B2,C4)+F3(C4)}=min{16,10}=10
S4:
k=1,有:
F1(A)=min{d1(A,B1)+F2(B1),d1(A,B2)+F2(B2)}=min{13,13}=13
因此由A点到E点的全过程的最短路径为A—>B2一>C4—>D3—>E。
最短路程长度为13。
从以上过程可以看出,每个阶段中,都求出本阶段的各个初始状态到过程终点E的最短路径和最短距离,当逆序倒推到过程起点A时,便得到了全过程的最短路径及最短距离,同时附带得到了一组最优结果(即各阶段的各状态到终点E的最优结果)。
在上例的多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有动态”的含义,称这
种解决多阶段决策最优化问题的方法为动态规划方法。
根据上例分析和动态规划的基本概念,可以得到动态规划的基本模型如下:
(1)确定问题的决策对象。
(2)对决策过程划分阶段。
(3)对各阶段确定状态变量。
(4)根据状态变量确定费用函数和目标函数。
(5)建立各阶段状态变量的转移过程,确定状态转移方程。
动态规划的基本知识
动态规划是研究一类最优化问题的方法,在经济、工程技术、企业管理、工农业生产及军事等领域中都有广泛的应用。
近年来,在ACM/ICPC中,使用动态规划(或部分应用动态规划思维)求解的题不仅
常见,而且形式也多种多样。
而在与此相近的各类信息学竞赛中,应用动态规划解题已经成为一种趋势,
这和动态规划的优势不无关系。
1、动态规划的常用名词
在学习动态规划之前,先得对下面的名词有所了解。
本书将标准名词作了一些简化,便于大家更
好的理解。
(1)状态(smte)
对于一个问题,所有可能到达的情况(包括初始情况和目标情况)都称为这个问题的一个状态。
(2)状态变量(sk)
对每个状态k关联一个状态变量Sk,它的值表示状态k所对应的问题的当前解值。
(3)决策(decision)
决策是一种选择,对于每一个状态而言,你都可以选择某一种路线或方法,从而到达下一个状态。
(4)决策变量(dk)
在状态k下的决策变量dk的值表示对状态k当前所做出的决策。
(5)策略
策略是一个决策的集合,在我们解决问题的时候,我们将一系列决策记录下来,就是一个策略,其中满足某些最优条件的策略称之为最优策略。
(6)状态转移函数(t)
从一个状态到另一个状态,可以依据一定的规则来前进。
我们用一个函数t来描述这样的规则,它将状态i和决策变量di映射到另一个状态j,记为t(i,di)=j
(7)状态转移方程(f)
状态转移方程f描述了状态变量之间的数学关系。
一般来说,与最优化问题相应,状态转移方程表示si的值最优化的条件,或者说是状态i所对应问题的最优解值的计算公式,用代数式表示就是:
Si=f({(Sj,dj)|i=t(j,dj),对决策变量dj所有可行的取值})
2、最优化原理
1951年美国数学家R.Bellman等人,根据一类多阶段问题的特点,把多阶段决策问题变换为一系列互相联系的单阶段问题,然后逐个加以解决。
一些静态模型,只要人为地引进“时间”因素,分成
时段,就可以转化成多阶段的动态模型,用动态规划方法去处理。
与此同时,他提出了解决这类问题的“最优化原理”(Principleofoptimality):
“一个过程的最优决策具有这样的性质:
即无论其初始状态和初始决策如何,其今后诸策略对以第一个决策所形成的状态作为初始状态的过程而言,必须构成最优策略”。
简言之,一个最优策略的子
策略,对于它的初态和终态而言也必是最优的。
这个“最优化原理”如果用数学化一点的语言来描述的话,就是:
假设为了解决某一优化问题,需
要依次作出n个决策Di,D2,…,Dn,如若这个决策序列是最优的,对于任何一个整数k,1不论前面k个决策是怎样的,以后的最优决策只取决于由前面决策所确定的当前状态,即以后的决策
Dk+1,Dk+2,…,Dn也是最优的。
最优化原理是动态规划的基础。
任何一个问题,如果失去了这个最优化原理的支持,就不可能用动态规划方法计算。
3、什么是动态规划
动态规划是运筹学的一个分支。
与其说动态规划是一种算法,不如说是一种思维方法来得更贴切。
因为动态规划没有固定的框架,即便是应用到同一道题上,也可以建立多种形式的求解算法。
许多隐式图上的算法,例如求单源最短路径的DijkStra算法、广度优先搜索算法,都渗透着动态规划的思想。
还有许多数学问题,表面上看起来与动态规划风马牛不相及,但是其求解思想与动态规划是完全一致的。
因此,动态规划不像深度或广度优先那样可以提供一套模式,需要的时候,取来就可以使用;它
必须对具体问题进行具体分析处理,需要丰富的想象力去建立模型,需要创造性的思想去求解。
4、动态规划适于解决什么样的问题
准确地说,动态规划不是万能的,它只适于解决一定条件的最优策略问题。
范围内的问题极多,还有许多看似不是这个范围中的问题都可以转化成这类问题。
上面所说的满足一定条件”主要指下面两点:
⑴状态必须满足最优化原理;
⑵状态必须满足无后效性。
所谓的无后效性是指:
过去的决策只能通过当前状态影响未来的发展,当前的状态是对以往决
策的总结”。
这条特征说明什么呢?
它说明动态规划适于解决当前决策和过去状态无关的问题。
状态,出现在策略的任何一个位置,它的地位都是相同的,都可以实施同样的决策。
这就是无后效性的内涵。
5、用动态规划解题的好处
说了这么多的动态规划,它到底给我们解题能带来什么好处呢?
其实动态规划的最大优势在于它具有极高的效率,而且动态规划还有其他的优势,例如:
动态规
划可以得出一系列解,算法清晰简便,程序易编、易调,等等。
目最优化原理与无后效性
多阶段决策问题”才
上面已经介绍了动态规划模型的基本组成,现在需要解决的问题是:
什么样的可以采用动态规划的方法求解
一般来说,能够采用动态规划方法求解的问题必须满足•最优化原理和•无后效性原则。
(1)动态规划的最优化原理。
作为整个过程的最优策略具有如下性质:
无论过去的状态和决策如何,对前面的决策所形成的当前状态而言,余下的诸决策必须构成最优策略。
可以通俗地理解为子问题的局部最优将导致整个问题的全局最优,即问题具有最优子结构的性质,也就是说一个问题的最优解只取决于其子问题的最优解,非最优解对问题的求解没有影响。
在例题1最短路径问题中,A到E的最优路径上的任一点到终点E的路径也必然是该点到终点E的一条最优路
径,满足最优化原理。
下面来讨论另外一个问题。
【例题2】余数最少的路径。
如图所示,有4个点,分别是A、B、C、D,相邻两点用两条连线C2k,C2k-1(1WkW表示两条通行的道路。
连线上的数字表示道路的长度。
定义从A到D的所有路径中,长度除以4所得余数最小的路径
为最优路径。
【分析】在这个问题中,如果还按照例题的最优取值可以由B的最优取值来确定,而
求一条最优路径。
1中的方法去求解就会发生错误。
按照例题1的思想,A
B的最优取值为(1+3)mod4=0,所以A的最优值应为2,
而实际上,路径C1—C3—C5可得最优值为(2+1+1)mod4=0,所以,B的最优路径并不是A的最优路径的子路径,也就是说,A的最优取值不是由B的最优取值决定的,即其不满足最优化原理,问题不具有最优子结构的性质。
由此可见,并不是所有的“决策问题”都可以用“动态规划”来解决,运用“动态规划”来处理问题必须满足最优化原理。
(2)动态规划的无后效性原则。
所谓无后效性原则,指的是这样一种性质:
某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响。
也就是说,“未来与过去无关”,当前的状态是此前
历史的一个完整总结,此前的历史只能通过当前的状态去影响过程未来的演变。
具体地说,如果一个问题被划分各个阶段之后,阶段I中的状态只能由阶段I+1中的状态通过状态转移方程得来,与其他状态没有关系,特别是与未发生的状态没有关系,这就是无后效性。
从图论的角度去考虑,如果把这个问题中的状态定义成图中的顶点,两个状态之间的转移定义为边,转移过程中的权值增量定义为边的权值,则构成一个有向无环加权图,因此,这个图可以进行“拓扑排序”,至少可以按他们拓扑排序的顺序去划分阶段。
看一看下面的两个具体例子。
【例题3】货郎担问题。
对于平面给定的n个点,编程确定一条连结各点的、闭合的游历路线问
题。
图中给出了7个点的情况问题的解。
【例题4】旅行路线问题。
在货郎担问题的基础上,若规定这种游历路线先从最左边开始,严格地由左至右到最右边的点,然后再严格地由右至左到出发点,求整个路程最短的路径长度。
图中给出了7个点问题的解。
例3图货郎担问题例4图旅行路线图
【分析】这两个问题看起来很非常相似,但本质上是完全不同的。
为了方便讨论,可以将每个顶点
标记号码。
由于必然经过最右边的顶点7,所以一条路(P1—P2)可以看做两条路(P1—7)与(P2—7)的结
合。
因此,这个题目的状态可以用两条道路结合的形式表示。
可以把这些状态中,两条路中起始顶点相同的状态归于一个阶段,设为阶段[P1,P2]。
那么,对于旅行路线问题来说,阶段[P1,P2]如果可以由阶段[Q1,Q2]推出,则必须满足的条
件就是:
Pl例如,阶段[3,4]中的道路可以由阶段[3,5]中的道路加一条边4—5得出,而阶段[3,5]的状态却无法由阶段[3,4]中的状态得出,因为在旅行路线问题的要求中必须严格地由左到右来旅行。
所以如果已经知道了阶段[3,4]中的状态,则阶段[3,5]中的状态必然已知,因此,
问题满足无后效性原则,可以考虑用动态规划方法求解。
而对于货郎担问题,阶段与阶段之间没有什么必然的“顺序”。
如道路{3—2—5—7,4—6—7}属
阶段
关系
图
于阶段[3,4],可由属于阶段[2,4]的道路{2—5—7,4—6—7}推出;而道路{2—3—6—7,4—5—7}属于阶段[2,4],可由属于阶段[3,4]的道路{3—6—7,4—5—7}推出。
如果以顶点表示阶段,推出关系表示边,那么,阶段[3,4]与阶段[2,4]对应的关系就如图右所示。
可以很清晰地看出,这
两个阶段的关系是“有后效性”的。
因为这个图中存在“环路”。
对于这个问题是不能像上一个问题那样来解决的。
动态规划的逆向思维法
动态规划是一种思维方法,没有统一的、具体的模式。
动态规划可以从多方面去考察,不同的方面对动态规划有不同的表述。
我们不打算强加一种统一的表述,而是从多个角度对动态规划的思维方法进行讨论,希望大家在思维具体问题时,也能够从多个角度展开,这样收获会更大。
逆向思维法是指从问题目标状态出发倒推回初始状态或边界状态的思维方法。
如果原问题可以分解成几个本质相同、规模较小的问题,很自然就会联想到从逆向思维的角度寻求问题的解决。
你也许会想,这种将大问题分解成小问题的思维不就是分治法吗?
动态规划是不是分而治之呢
其实,虽然我们在运用动态规划的逆向思维法和分治法分析问题时,都使用了这种将问题实例归纳为更小的、相似的子问题,并通过求解子问题产生一个全局最优值的思路,但动态规划不是分治法:
关键在于分解出来的各个子问题的性质不同。
分治法要求各个子问题是独立的(即不包含公共的子问题),因此一旦递归地求出各个子问题的解后,便可自下而上地将子问题的解合并成原问题的解。
如果各子问题是不独立的,那么分治法就要做许多不必要的工作,重复地解公共的子问题。
动态规划与分治法的不同之处在于动态规划允许这些子问题不独立(即各子问题可包含公共的子
问题),它对每个子问题只解一次,并将结果保存起来,避免每次碰到时都要重复计算。
这就是动态规划高效的一个原因。
动态规划的逆向思维法的要点可归纳为以下三个步骤:
(1)分析最优值的结构,刻画其结构特征;
(2)递归地定义最优值;
(3)按自底向上或自顶向下记忆化的方式计算最优值。
【例题5】背包问题描述:
有一个负重能力为m的背包和n种物品,第i种物品的价值为v[i],重量为w[i]。
在不超过背包负重能力的前提下选择若干个物品装入背包,使这些的物品的价值之和最大。
每种物品可以不选,也可以选择多个。
假设每种物品都有足够的数量。
分析:
从算法的角度看,解决背包问题一种最简单的方法是枚举所有可能的物品的组合方案并计算这个组合方案的价值之和,从中找出价值之和最大的方案。
显然,这种靠穷举所有可能方案的方法不是一种有效的算法。
但是这个问题可以使用动态规划加以解决。
下面我们用动态规划的逆向思维法来分析这个问题。
(1)背包问题最优值的结构
包含其子问题的最优值,问题的这种性质称为最优子结构。
一个问题的最优子结构性质是该问题可以使用动态规划的显著特征。
对一个负重能力为m的背包,如果我们选择装入一个第i种物品,那么原背包问题就转化为负
重能力为m-w[i]的子背包问题。
原背包问题的最优值包含这个子背包问题的最优值。
若我们用背包的负重能力来划分状态,令状态变量s[k]表示负重能力为k的背包,那么s[m]的值只取决于s[k](k因此背包问题具有最优子结构。
(2)递归地定义最优值
动态规划的逆向思维法的第二步是根据各个子问题的最优值来递归地定义原问题的最优值。
对背
包问题而言,有状态转移方程:
/max{s[k-w[i]]+v[i]}(其中KiWn且k-w[i]>0)
s[k]=若k>0且存在1,0
\0否则。
有了计算各个子问题的最优值的递归式,我们就可以直接编写对应的程序。
下述的函数knapsack
是输入背包的负重能力k,返回对应的子背包问题的最优值s[k]:
functionknapsack(k:
integer):
integer;
begin
knapsack:
=0;
fori:
=1tondo
ifk-w[i]>=0then
begin
t:
=knapsack(k-w[i])+v[i];
ifknapsack=t;
end;
end;
上述递归算法在求解过程中反复出现了一个子问题,且对每次重复出现的子问题都要重新解一次,这需要多花费不少时间。
下面先考虑一个具体的背包问题。
例如,当
m=3,n=2,v[1]=1,w[1]=1,v[2]=2,w[2]=2,
图1示出了由调用knapsack(3)所产生的递归树,每一个结点上标有参数k的值,请注意某些数出现了
多次。
/\
21
/、\
100
/
0
图1
例如,knapsack
(1)被引用了两次:
在计算knapsack(3)和knapsack
(2)中分别被引用;而knapsack(O)更是被引用了三次。
如果knapsack
(1)和knapsack(0)每次都要被重新计算,则增加的运行时间相当可观。
下面,我们来看看动态规划是如何解决这个问题的。
(3)按自顶向下记忆化或自底向上的方式求最优解
一般地,如果一个解最优化问题的递归算法经常反复地解重复的子问题,而不是总在产生新的子问题时,我们说该最优化问题包含重迭子问题。
这类问题不宜用分治法求解,因为分治法递归的每一步要求产生相异的子问题。
在这种情况下,采用动态规划是很合适的,因为该方法对每一个子问题只解一次,然后把解存放在一个表中,以便在解同样的子问题时查阅,充分利用了重迭子问题。
一般来说,解决重迭子问题的方式有两种。
①自顶向下的记忆化方式
该方式的程序流程基本按照原问题的递归定义,不同的是,它专门设置了一张表,以记忆在求解过程中得出的所有子问题的解。
一个记忆的递归算法为每个子问题的解在表中记录一个表项。
初始时每个表项都包含一个特殊值,以示该表项的解有待填入。
例如背包问题中:
s[i]=-1;(0当在递归算法的执行中第一次遇到一个子问题时(即s[i]=-1),计算其解并填入表中,以后每遇到该子问
题,只要查看表中先前填入的值即可。
下面,我们按照自上而下的记忆化方式,写出求背包问题的递归算法。
函数memorized_knapsack输入背包的负重能力k,返回背包问题的解s[k]。
s表应设为全局变量,使
得函数执行后,传出所有子问题的解s[i](0wiwm)
functionmemorized_knapsack(k:
integer):
integer;
begin
ifs[k]=-1then
begin
s[k]:
=0;
fori:
=1tondo
ifk-w[i]>=0then
begin
t:
=memorized_knapsack(k-W[i])+V[i];
ifs[k]=t;
end;
end;
memorized_knapsack:
=s[k];
end;
我们在主程序通过调用memorized_knapsack(m)即可获得背包问题的解。
显然这种自顶向下记忆化的算法效率比重复解重迭子问题的knapsack算法要强一些。
此外,在程序中
加入判别哪些子问题需要求解的语句,只解那些肯定要解的子问题,从而节省了时间。
②自底向上的方式
假设我们要解决在分析函数knapsack当中提出的那个具体的背包问题。
观察一下在调用
memorized_knapsack(4)的过程中,s[k]被计算出来的顺序。
我们发现,首先是s[0]被计算出来,然后是s[1],s[2],最后s[3]被计算出来,这与调用的次序正好相反。
动态规划的执行方式是自底向上,所有的子问题计算一次,充分利用重迭子问题。
因此,我们很自然就想到与这种自底向上的执行方式相应的采用自底向上的方式递推所有子问题的最优值。
我们知道,计算负重能力为k的背包问题的解仅依赖于计算负重能力小于k的背包问题的解,因此填
s表的方式与解决负重能力递增的背包问题相对应:
最初设定负重能力为0的背包问题的解s[0]=0。
然后依次考虑负重能力为1,2,…,m的背包问题的
解。
每填入一个s[k](0s[k-w[i]](00)
下面是按照自底向上方式计算背包问题的算法流程。
过程knapsack_order输入背包的负重能力k,s
表设为全局变量。
过程执行后所有子问题的解通过s表传给主程序。
procedureknapsack_order(k:
integer);
begin
fori:
=1tokdos[i]:
=0;
forj:
=1tokdo
fori:
=1tondo
ifj-w[i]>=0then
begin
t:
=s[j-w[i]]+v[i];
ifs[j]=t;
end;
end;
我们在主程序调用knapsack_order(m),过程执行后s[m]即为背包问题的解。
最后,我们分析一下背包问题的动态规划解法的复杂度。
所需的存储开销主要在s表上,其空间复杂
度是O(m)。
而整个s表用两重循环求出,循环内语句的执行只需常数时间,因此,时间复杂度是O(m,n)。
自顶向下的记忆化是动态规划的一种变形。
动态规划的执行方式是自底向上,因此自底向上的计算方式是与动态规划的执行方式相一致的。
它无需递归代价,且维护记忆表的开销也要小些,因此其效率通常好于自顶向下的记忆法。
但是,在动态规划的执行过程中,并不是所有的子问题都要用到它。
对某个具体问题而言,可能有大部分子问题的最优值是不必计算的。
自底向上的计算方式无法判断那些子问题是需要求解的,所以它将一视同仁地处理所有的子问题,这就可能会把大量的时间都花在计算不必解决的子问题上;而自顶向下的记忆法可以判断那些子问题是需要求解的,只解那些肯定要解的子问题,在这个意义上,自顶向下的算法效率又好于自底向上。
所以到底那种方式效率更高,我们要具体问题具体分析。
最后,给出求解背包问题的程序如下:
{//背包问题程序}
programknapsack;
const
maxn=100;
maxm=1000;
var
m,n:
integer;
S:
array[0..maxm]ofinteger;
v,w:
array[1..maxn]ofinteger;
{//输入数据}
procedureread_data