[转帖]算法中难度最大的——动态规划

算法——动态规划法
                                                                                                       ——运用之妙,存乎一心
一、引言
  动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decisionprocess)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957年出版了他的名著Dynamic Programming,这是该领域的第一本著作。
   动态规划问世以来,在经济管理、生产调度、工程技术和最优控制等方面得到了广泛的应用。例如最短路线、库存管理、资源分配、设备更新、排序、装载等问题,用动态规划方法比用其它方法求解更为方便。
虽然动态规划主要用于求解以时间划分阶段的动态过程的优化问题,但是一些与时间无关的静态规划(如线性规划、非线性规划),只要人为地引进时间因素,把它视为多阶段决策过程,也可以用动态规划方法方便地求解。
动态规划法的定义:在求解问题中,对于每一步决策,列出各种可能的局部解,再依据某种判定条件,舍弃那些肯定不能得到最优解的局部解,在每一步都经过筛选,以每一步都是最优解来保证全局是最优解,这种求解方法称为动态规划法。
动态规划是所有算法设计方法中难度最大的一种。
二、动态规划的基本思想
一般来说,只要问题可以划分成规模更小的子问题,并且原问题的最优解中包含了子问题的最优解(即满足最优子化原理),则可以考虑用动态规划解决。
动态规划的实质是分治思想和解决冗余,因此,动态规划是一种将问题实例分解为更小的、相似的子问题,并存储子问题的解而避免计算重复的子问题,以解决最优化问题的算法策略。
由此可知,动态规划法与分治法和贪心法类似,它们都是将问题实例归纳为更小的、相似的子问题,并通过求解子问题产生一个全局最优解。其中贪心法的当前选择可能要依赖已经作出的所有选择,但不依赖于有待于做出的选择和子问题。因此贪心法自顶向下,一步一步地作出贪心选择;而分治法中的各个子问题是独立的 (即不包含公共的子子问题),因此一旦递归地求出各子问题的解后,便可自下而上地将子问题的解合并成问题的解。但不足的是,如果当前选择可能要依赖子问题的解时,则难以通过局部的贪心策略达到全局最优解;如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题。
解决上述问题的办法是利用动态规划。该方法主要应用于最优化问题,这类问题会有多种可能的解,每个解都有一个值,而动态规划找出其中最优(最大或最小)值的解。若存在若干个取最优值的解的话,它只取其中的一个。在求解过程中,该方法也是通过求解局部子问题的解达到全局最优解,但与分治法和贪心法不同的是,动态规划允许这些子问题不独立,(亦即各子问题可包含公共的子子问题)也允许其通过自身子问题的解作出选择,该方法对每一个子问题只解一次,并将结果保存起来,避免每次碰到时都要重复计算。
因此,动态规划法所针对的问题有一个显著的特征,即它所对应的子问题树中的子问题呈现大量的重复。动态规划法的关键就在于,对于重复出现的子问题,只在第一次遇到时加以求解,并把答案保存起来,让以后再遇到时直接引用,不必重新求解。
一般来说,适合于用动态规划法求解的问题具有以下特点:
1、可以划分成若干个阶段,问题的求解过程就是对若干个阶段的一系列决策过程。
2、每个阶段有若干个可能状态
3、一个决策将你从一个阶段的一种状态带到下一个阶段的某种状态。
4、在任一个阶段,最佳的决策序列和该阶段以前的决策无关。
5、各阶段状态之间的转换有明确定义的费用,而且在选择最佳决策时有递推关系(即动态转移方程)。
动态规划法所处理的问题是一个多阶段最优化决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线。
在学习动态规划法之前,我们先来了解动态规划的几个概念
1、  阶段:把问题分成几个相互联系的有顺序的几个环节,这些环节即称为阶段。
2、  状态:某一阶段的出发位置称为状态。
3、  决策:从某阶段的一个状态演变到下一个阶段某状态的选择。
4、  状态转移方程:前一阶段的终点就是后一阶段的起点,前一阶段的决策选择导出了后一阶段的状态,这种关系描述了由k阶段到k+1阶段状态的演变规律,称为状态转移方程。
动态规划算法的基本步骤
设计一个标准的动态规划算法,通常可按以下几个步骤进行:
划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。注意这若干个阶段一定要是有序的或者是可排序的(即无后向性),否则问题就无法用动态规划求解。
选择状态:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
确定决策并写出状态转移方程:之所以把这两步放在一起,是因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以,如果我们确定了决策,状态转移方程也就写出来了。但事实上,我们常常是反过来做,根据相邻两段的各状态之间的关系来确定决策。
写出规划方程(包括边界条件):动态规划的基本方程是规划方程的通用形式化表达式。一般说来,只要阶段、状态、决策和状态转移确定了,这一步还是比较简单的。
动态规划的主要难点在于理论上的设计,一旦设计完成,实现部分就会非常简单。根据动态规划的基本方程可以直接递归计算最优值,但是一般将其改为递推计算,实现的大体上的框架如下:
标准动态规划的基本框架
1.  对fn+1(xn+1)初始化;    {边界条件}
2.  for k:=n downto 1 do
3.      for 每一个xk∈Xk do
4.        for 每一个uk∈Uk(xk) do
            begin
5.            fk(xk):=一个极值;                 {∞或-∞}
6.            xk+1:=Tk(xk,uk);                  {状态转移方程}
7.            t:=φ(fk+1(xk+1),vk(xk,uk));       {基本方程(9)式}
8.            if  t比fk(xk)更优 then fk(xk):=t; {计算fk(xk)的最优值}
           end; 
9.  t:=一个极值;                               {∞或-∞}
10. for 每一个x1∈X1 do
11.     if f1(x1)比t更优 then t:=f1(x1);       {按照10式求出最优指标}
12. 输出t;
但是,实际应用当中经常不显式地按照上面步骤设计动态规划,而是按以下几个步骤进行:
分析最优解的性质,并刻划其结构特征。
递归地定义最优值。
以自底向上的方式或自顶向下的记忆化方法(备忘录法)计算出最优值。
根据计算最优值时得到的信息,构造一个最优解。
步骤(1)--(3)是动态规划算法的基本步骤。在只需要求出最优值的情形,步骤(4)可以省略,若需要求出问题的一个最优解,则必须执行步骤(4)。此时,在步骤(3)中计算最优值时,通常需记录更多的信息,以便在步骤(4)中,根据所记录的信息,快速地构造出一个最优解。
三、动态规划与其他算法的比较
动态规划与其说是一种算法,不如说是一种算法设计的策略,他的基本思想体现于许多其它算法之中。下面我们通过比较动态规划和其他的一些算法之间的相互联系,来深入理解动态规划的基本思想。
动态规划与静态规划——某些情况下可以相互转化
动态规划与递推——动态规划是最优化算法
动态规划与搜索——动态规划是高效率、高消费算法
动态规划与网络流——动态规划是易设计易实现算法
四、动态规划的理论模型
在动态规划算法发展的初期,Bellman从纯粹的逻辑出发给出了最优性原理——Principle of Optimality:
"An optimal policy has the property that whatever the initial stateand initial decision are, then remaining decisions must constitute anoptimal policy with regard to the state resulting from first decision."
他给出这个原理作为动态规划适用的条件,后来Morin在1982年证明了这只是一个充分条件而非必要条件。
动态规划开始只是应用于多阶段决策性问题,后来渐渐被发展为解决离散最优化问题的有效手段,进一步应用于一些连续性问题上。然而,动态规划更像是一种思想而非算法,它没有固定的数学模型,没有固定的实现方法,其正确性也缺乏严格的理论证明。因此,一直以来动态规划的数学理论模型是一个研究的热点。
目前比较流行的主要有两种理论模型:关系计算模型(relational calculusmodel)和估价网络模型(valuation network model)。
五、实例
有了以上的设计模式,对于简单的动态规划问题,就可以按部就班地进行动态规划设计。
例1、拦截导弹 (问题来源: 1999 年全国青少年信息学(计算机)奥林匹克分区联赛高中组复赛试题第一题)
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度.某天,雷达捕捉到敌国的导弹来袭.由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹.
输入导弹依次飞来的高度(雷达给出的高度数据是不大于 30000 的正整数),计算这套系统最多能拦截多少导弹,并依次输出被拦截的导弹飞来时候的高度.
样例:
INPUT
389 207 155 300 299 170 158 65
OUTPUT
6 (最多能拦截的导弹数)
389 300 299 170 158 65
分析: 因为只有一套导弹拦截系统,并且这套系统除了第一发炮弹能到达任意高度外,以后的每一发炮弹都不能高于前一发炮弹的高度;所以,被拦截的导弹应该按飞来的高度组成一个非递增序列.题目要求我们计算这套系统最多能拦截的导弹数,并依次输出被拦截导弹的高度,实际上就是要求我们在导弹依次飞来的高度序列中寻找一个最长非递增子序列.
设 X={x 1 ,x 2 ,…,x n } 为依次飞来的导弹序列, Y={y 1 ,y 2 ,…,y k } 为问题的最优解(即 X 的最长非递增子序列), s 为问题的状态(表示导弹拦截系统当前发送炮弹能够到达的最大高度,初值为 s=∞—— 第一发炮弹能够到达任意的高度).如果 y 1 =x 1 ,即飞来的第一枚导弹被成功拦截.那么,根据题意"每一发炮弹都不能高于前一发的高度",问题的状态将由 s=∞ 变成 s≤x 1 ( x 1 为第一枚导弹的高度);在当前状态下,序列 Y 1 ={y 2 ,…,y k } 也应该是序列 X 1 ={x 2 ,…,x n } 的最长非递增子序列(大家用反证法很容易证明).也就是说,在当前状态 s≤x 1 下,问题的最优解 Y 所包含的子问题(序列 X 1 )的解(序列 Y 1 )也是最优的.这就是拦截导弹问题的最优子结构性质.
设 D(i) 为第 i 枚导弹被拦截之后,这套系统最多还能拦截的导弹数(包含被拦截的第 i 枚).我们可以设想,当系统拦截了第 k 枚导弹 x k ,而 x k 又是序列 X={x 1 ,x 2 ,…,x n } 中的最小值,即第 k 枚导弹为所有飞来的导弹中高度最低的,则有 D(k)=1 ;当系统拦截了最后一枚导弹 x n ,那么,系统最多也只能拦截这一枚导弹了,即 D(n)=1 ;其它情况下,也应该有 D(i)≥1 .根据以上分析,可归纳出问题的动态规划递归方程为:
假设系统最多能拦截的导弹数为 dmax (即问题的最优值),则
dmax ( i 为被系统拦截的第一枚导弹的顺序号)
所以,要计算问题的最优值 dmax ,需要分别计算出 D(1) , D(2) ,…… D(n) 的值,然后将它们进行比较,找出其中的最大值.根据上面分析出来的递归方程,我们完全可以设计一个递归函数,采用自顶向下的方法计算 D(i) 的值.然后,对 i 从 1 到 n 分别调用这个递归函数,就可以计算出 D(1) , D(2) ,…… D(n) .但这样将会有大量的子问题被重复计算.比如在调用递归函数计算 D(1) 的时候,可能需要先计算 D(5) 的值;之后在分别调用递归函数计算 D(2) , D(3) , D(4) 的时候,都有可能需要先计算 D(5) 的值.如此一来,在整个问题的求解过程中, D(5) 可能会被重复计算很多次,从而造成了冗余,降低了程序的效率.
其实,通过以上分析,我们已经知道: D(n)=1 .如果将 n 作为阶段对问题进行划分,根据问题的动态规划递归方程,我们可以采用自底向上的方法依次计算出 D(n-1) , D(n-2) ,…… D(1) 的值.这样,每个 D(i) 的值只计算一次,并在计算的同时把计算结果保存下来,从而避免了有些子问题被重复计算的情况发生,提高了程序的效率.算法代码如下:
for i=1 to n
D(i)=1
next i
for i=n-1 to 1 step -1
for j=i+1 to n
if x(j)<=x(i) and D(i)dmax then
dmax=D(i)
xh=i
endif
next j
next i
我们在计算最优值时保存了被拦截的第一枚导弹的顺序号 xh .接下来通过这个信息构造问题的最优解(由所有被拦截的导弹的高度组成的非递增序列).算法代码如下:
print x(xh)
for i=xh+1 to n
if x(i)<=x(i-1) then
print x(i)
endif
next i
例2:合唱队形(noip2004tg)
【问题描述】N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1, 2, …, K,他们的身高分别为T1, T2, …, TK,则他们的身高满足T1 < T2 < … < Ti , Ti > Ti+1 > … > TK (1 <= i <= K)。你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
【输入文件】输入文件chorus.in的第一行是一个整数N(2 <= N <= 100),表示同学的总数。第一行有n个整数,用空格分隔,第i个整数Ti(130 <= Ti <= 230)是第i位同学的身高(厘米)。
【输出文件】输出文件chorus.out包括一行,这一行只包含一个整数,就是最少需要几位同学出列。
【样例输入】8
186 186 150 200 160 130 197 220
【样例输出】 4
算法分析:此题采用动态规划法求解。先分别从左到右求最大上升子序列,从右到左求最大下降子序列,再枚举中间最高的一个人。算法实现起来也很简单,时间复杂度O(N^2)。
我们先考虑如何求最大上升子序列的长度,设f1(i)为前i个同学的最大上升子序列长度。若要求f1(i),必须先求得f1(1),f1(2),…,f1(i-1),再选择一个最大的f1(j)(jf1(i)=max{f1(j)+1} (j边界条件:f1(1)=1;
设f2(i)为后面N-i+1位排列的最大下降子序列长度,用同样的方法可以得到状态转移方程:f2(i)=max{f2(j)+1} (i有了状态转移方程,程序实现就非常容易了。
源程序:
var
  t,f1,f2:array[1..100]of byte;
  i,j,n,max:integer;
begin
  assign(input,'chorus.in');
  reset(input);  readln(n);
  for i:=1 to n do begin
read(t[i]);f1[i]:=1;f2[i]:=1;
end;{for}
  close(input); max:=0;
  for i:=2 to n do
    for j:=1 to i-1 do begin
      if (t[i]>t[j])and(f1[j]>=f1[i]) then f1[i]:=f1[j]+1; {从左到右求最大上升子序列}
      if (t[n-i+1]>t[n-j+1])and(f2[n-j+1]>=f2[n-i+1]) then f2[n-i+1]:=f2[n-j+1]+1; {从右到左求最大下降子序列}
    end;{for}
  for i:=1 to n do if max  assign(output,'chorus.ans');
rewrite(output);
  writeln(n-max+1);
close(output);
end.
例3、农场实习
[问题描述]
公益劳动回来之后,小可可又进入了第二阶段的毕业实习。这次小可可被安排去农场劳动。
农场的小河边上沿着河岸长了很多西瓜,小可可和一个老师的任务就是去摘西瓜。
老师又一个摘西瓜的怪癖:总是沿着河岸边走边摘,第一个被摘西瓜是哪一个完全凭老师高兴了,没有什么固定规则,但以后摘的每一个西瓜都不能比上一个小。
例如,沿河岸有4个西瓜,大小依次是3、6、2、4。
如果老师首先摘的是第一个西瓜(3),则他们能摘的大小为3、6或3、4的西瓜。
如果首先摘的是第二个西瓜 ,则他们只能摘这个西瓜……
现在知道了沿河岸各个西瓜的大小,你能算出老师和小可可沿着河岸走一遍之后,至少有多少个西瓜被遗漏而不能摘下来吗?
[输入]第一个数是西瓜的个数c(c<=10的6次方),随后的c个数依次是沿着河岸的各个西瓜大小(各个西瓜的大小都不能超过10的8次方)
[输出]输出遗漏不能摘的西瓜个数最小值u(u<=1000)。
[例如]
输入
4 3 6 2 4
输出
2
例4、乘积最大(noip2000tg)
题目大意:在一个长度为N的数字串中插入r个乘号,将它分成r+1个部分,找出一种分法,使得这r+1个部分的乘积最大。
算法分析:此题满足动态规划法的求解标准,我们把它按插入的乘号数来划分阶段,若插入K个乘号,可把问题看做是K个阶段的决策问题。设f[i,k]表示在前i位数中插入K个乘号所得的最大值,a[i,j]表示从第i位到第j位所组成的自然数。用f[i,k]存储阶段K的每一个状态,可以得状态转移方程:
f[i,k]=max{f[j,k-1]*a[j+1,i]}(k<=j<=i)
边界值:f[j,0]=a[1,j] (1根据状态转移方程,我们就很容易写出动态规划程序:
for k:=1 to r do
  for i:=k+1 to n do
    for j:=k to I do
       if f[i,k]
源程序(略)。
例5、合并石子问题
在一个圆形操场的四周摆放着n堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的两堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。试设计一个算法,计算出将n堆石子合并成一堆的最小得分和最大得分,并分析算法的计算复杂性
分析:比如有3堆,石子个数分别为1,2,3。那么先合并1,2堆,再与第3堆合并的得分是(1+2)+(3+3)=9。先合并2,3堆,再与第1堆合并的得分是(2+3)+(5+1)=11。f[i,j]表示从第i个起接下来j个合并的的最大值d[i,j]表示第i到第j个的价值和,则f[i,j]=max{[i,k]+f[i+k,j-k]+d[i,j]},f[i,1]=0。
最小分跟huffman最小生成树是同一个原理,算法就是最小生成树算法,把权最小的先合并,并把和做为新堆的权,重复合并,最后生成一棵树,根部的权就是得的分。最大分也应该类似。
六、总结
近年来,涉及动态规划的各种竞赛题越来越多,每一年的NOIP都至少有一道题目需要用动态规划法求解。而应用动态规划法解题是富于技巧性和创造性的,虽然在前面的求解过程中给出了一个解题的基本模式,但由于动态规划题目出现的形式多种多样,并且大部分题目表面上看不出与动态规划的直接联系,只有在充分把握其思想精髓的前提下大胆联想,多做多练才能达到得心应手,灵活运用的境界。
 

你可能感兴趣的