1.动态规划概述
1.1 什么是动态规划
相信大家都遇到过这样一个问题:
例题1 如图1,有一个有向图,图中有n层顶点,除了第n层以外,第i层与第i+1层之间有边。边上的数值代表两个顶点的距离。要求找一条从第1层到第n层的路径,使得走过的边的和最短。
算法分析
解决这个问题有3种思路。
第1种:回溯算法。直接列举出从A到J的所有路径,并且求出所有路径当中走过距离最短的。如果假设这个图是一个满二叉树的话,这个算法的效率为2n。很明显,当n增长的时候,这个算法的效率不容乐观。
第2种:采用狄杰斯特拉算法,求这个有向图的最短路径。如果这个图有v个顶点的话,这个算法的效率为v2,已经很优秀了。
但是,这个问题有没有更好地方法呢?
假如我们把这个图按层次分一下(如图2):
不难发现,从阶段1到阶段2,阶段2到阶段3等等都是互相独立的,根据题目的意义,不会出现阶段2有一条边指向阶段3。
而从阶段i到阶段i+1的最短距离一定是 。而再仔细看,如果从阶段1到阶段3的最短路径,一定经过阶段2的最短路径。
再看一个命题:
如图3,若AB、BC为每个阶段的最优选择,那么,有ABC 为最优选择。
证明: 设有一条路径ADC,其长度和小于ABC。即要满足AD<AB; DC<BC; 但是,由于AB、BC为每个阶段的最优选择,所以与题设矛盾,故ABC 为最优选择。
所以,得到如下递推式:
题目就圆满地解决了。时间复杂度为n2。
这个问题的解决过程中,我们看到:在多阶段的问题中,各阶段的决策依赖以来于当前状态,又引起状态的转移,这种解决多阶段问题的算法称为动态规划。
1.2 动态规划中的几个概念
阶段:对于一个问题,可按其题目意思划分出各次选择的部分,这样的部分就是阶段。
状态:某一阶段的初始位置为这一阶段的状态。
决策:在对问题处理中做出选择性的行动。
状态转移方程:前一阶段的终点就是后一阶段的起点,前一阶段的决策选择导出了后一阶段的状态,这种关系描述了由k阶段到k+1阶段状态的演变规律,称为状态转移方程。
1.3 动态规划的条件
既然动态规划算法是将问题划分成若干个可以独立解决的子部分,然后再进行汇总,那么,它一定要满足一定的条件。
最优化原则
一个最优化策略的子策略总是最优的。也就是说,对于目前的决策而言,余下的决策一定是最优的。否则这个题目将不能用动态规划算法来解决。
无后效性
当前决策与过去的状态没有任何关联。也就是说,对于目前的决策而言,不能够影响到过去的决策。否则这个题目将不能用动态规划算法来解决。
2. 动态规划算法的设计
2.1问题的后效性、最优化原则
看如下一个问题:
例题2 如图4。有一个有向图。要求求出给出顶点的最短路径。
算法分析 此题十分典型,可以使用图的最短路径算法。但是,此题是否可以使用动态规划算法呢?
否。因为它某一次策略的子策略并不一定是最优的。例如AG很有可能没有ABG优。
所以不难看出,我们把问题划分之后,如果这个问题的某一个决策的子决策不一定是最优的,那么,这种划分方法下的动态规划就不一定能得到最优解。
同样,如果一个问题划分阶段后,某一个阶段的改变仍旧能够影响前阶段的最优解,那么这个问题的这种划分就不满足无后效性原则,那么,这种划分方法下的动态规划就不一定能得到最优解。
2.2 状态转移方程的列式
根据动态规划的无后效性原则与最优化原则,我们可以列出状态转移方程的一般形式
而实际上,解决实际问题的状态转移方程要比这个简单形式复杂的多,但是,不论实际应用的动态规划是几维的,有多少种复杂的决策,都可以用这个公式来表示。
根据以上公式,我们可以看出,动态规划算法的复杂度是次方级。一般地,动态规划算法的效率是 阶段数*状态数*决策数。 也就是说,动态规划的一般效率将是O(kux)。相对于回溯等搜索算法,这个时间复杂度是非常的小的。
2.3 动态规划的实现
自上而下计算
直接将数值代入状态转移方程,进行递归。
自下而上计算
这是设计动态规划程序最常用的一种方法。这种方法从阶段1开始进行决策,推出阶段2的状态,再有阶段2开始决策……这样避免了递归代入,节省了堆栈的空间。这样可以得到算法流程:
说明:
*如果每一个阶段只有一个出发位置,则可通过一重循环来枚举各阶段的状态
*如果决策较少,则可以将第三重循环写成If、Case等语句的形式
3.动态规划的应用
例题3防卫导弹
一种新型的防卫导弹可截击多个攻击导弹。它可以向前飞行,也可以用很快的速度向下飞行,可以毫无损伤地截击进攻导弹,但不可以向后或向上飞行。但有一个缺点,尽管它发射时可以达到任意高度,但它只能截击比它上次截击导弹时所处高度低或者高度相同的导弹。现对这种新型 防卫导弹进行测试,在每一次测试中,发射一系列的测试导弹(这些导弹发射的间隔时间固定,飞行速度相同),该防卫导弹所能获得的信息包括各进攻导弹的高度,以及它们发射次序。现要求编一程序,求在每次测试中,该防卫导弹最多能截击的进攻导弹数量,一个导弹能被截击应满足下列两个条件之一:
1、它是该次测试中第一个被防卫导弹截击的导弹;
2、它是在上一次被截击导弹的发射后发射,且高度不大于上一次被截击导弹的高度的导弹。
输入格式:从当前目录下的文本文件”CATCHER.DAT”读入数据 。该文 件的第一行是一个整数N(0〈=N〈=4000),表示本次测试中,发射的进攻导弹数,以下N行每行各有一个整数hi(0〈=hi〈=32767),表示第i个进攻导弹的高度。文件中各行的行首、行末无多余空格,输入文件中给出的导弹是按发射顺序排列的。
输出格式:答案输出到当前目录下的文本文件”CATCHER.OUT”中,该文件第一行是一个整数max,表示最多能截击的进攻导弹数,以下的max行每行各有一个整数,表示各个被截击的进攻导弹的编号(按被截击的先后顺序排列)。输出的答案可能不唯一,只要输出其中任一解即可。
算法分析 这一题要用搜索来做,问题的时间复杂度是趋近于2n-1,当n=4000的时候,这个数据将大得不能忍受。
继续分析,我们发现,我们每拦截一颗导弹,如果拦截这一颗导弹前的几颗导弹不是最优的,那么,它本身一定不是最优的。那么,我们发现,本问题具有最优化性质,且切没有后效性。而归结为状态转移方程就是:
这种算法的时间复杂度为O(n2),这是完全可以承受的。
小结 用动态规划解决问题,主要是在分析。对问题良好的阶段划分,也在于分析。分析问题是动态规划解决问题的本质。
有些问题,并没有按照动态规划的标准形式去列式(没有max、min),却仍旧应用了动态规划的思想。应用了动态规划思想来解决题目,往往会得到事半功倍的效果。
例题4砝码称重
有6种砝码:1g,2g,5g,10g,20g,50g。砝码只允许放在天平的右盘。问用这些砝码一共能够称出多少重量?(假设砝码总重不超过1000g)。
算法分析 这个问题是分区联赛中的一个问题。相信大多数同学当时都是用搜索算法来解决的。而如果此题的限制砝码总重如果是10000g的话,想必大多数同学的算法都应该“超时”了。
事实上,我们进一步分析题目:对于一个已有的重量w,不管它是怎么得到的,有w + weight[c[k]]能够得到。当然,前提条件是c[k]>0。这样,我们很容易地发现,这个记数问题可以用动态规划来解决:
c[j]能够称出c[j + a[i]],其中a[i]为所有可以称得的重量。
这个算法的时间复杂度是O(n*v)。其中n是砝码的数量,v是最大的重量。而此题的时间复杂度是O(1000n)。虽然系数比较大,但是它仍然是一个很优秀的算法。比起搜索的次方级,要好得多。但是,此题的空间复杂度很高,是O(v)。如果v特别大的时候,还应该考虑对它进行压缩。
小结 事实上,解决这个题目并没有真正“动态”地规划,只是利用了动态规划的思想,将所有可能求出再打印。动态规划的思想在很多题目上能够适用(例如很多递推问题)。
4.动态规划的延伸
4.1 动态规划的灵活应用
我们先看一个例题:
例题5方格取数
有一个n*m的方格。一个人在(1,1)点。第一次,这个人从(1,1)点走到(n,m)点,这时这个人只能向下或向右走,并且取掉方格中的数字;第二次,这个人从(n,m)点走到(1,1)点,这时这个人只能向左或向上右走,并且再取掉方格中的数字。要求:这个人两次走过的路径和最大。
算法分析 这个问题也是分区联赛中的一个问题。当时有一部分同学采用了一个似乎很“经典”的办法:
第一次,对整个矩阵做一次动态规划1,求出当前格的最大代价,然后再做一次,把两次的代价和加起来,就是方格取数的最大代价。此时应用的公式是:
但是,实际上,这是一种贪心算法。
虽然它保证了两次走都是最优,但是不能保证总体最优。
考虑只走一次的情况,只需要考虑一个人到达某个格子(i,j)的情况,得到:
考虑两个人同时从(1,1)出发,不就是一个人走两次吗?这样需要考虑两个格子的情况。这样就有4种情况。
这样,问题就解决了。
小结 动态规划只是一种思想方法,解题思路,而不是一种套路。用动态规划解决问题应该灵活应用,才会在解体上收到很好的效果。
4.2 动态规划与搜索
谈到动态规划与搜索,我要先提一提贪心算法。虽然贪心算法在大多数情况下都不能直接得到最优解,但是,它仍旧可以与搜索算法并用。具体有如下用处:
(1)在解决某些最优化问题中,利用贪心算法在极短的时间(贪心算法的时间复杂度几乎为O(n)或O(1))内得到一组较优解。于是定出搜索的边界,可以剪去大量的分支。
(2)搜索的某一个部分可以采用贪心来解决(比如说某些问题的最后几步),大大节省了搜索的时间,提高了效率。
同样的,我们从例题5当中可以看到,单纯的动态规划有时候并不能够得到最优解。但是,它得到的解也是比较好的(毕竟它专门取了两次最大值)。所以,我们可以同样把动态规划替换 用处(1) 中的贪心算法,得到很好的效率。当然,动态规划算法也能够替换 用处(2)中的贪心算法。毕竟,动态规划所求到的最优解的可靠性要比贪心大得多。
但是,搜索与动态规划却有着更精妙的联系。我们看如下一个例题:
例题6翻棋子
有一个棋子,其1、6面2、4面3、5面相对。现给出一个M*N的棋盘,棋子起初处于(1,1)点,摆放状态给定,现在要求用最少的步数从(1,1)点翻滚到(M,N)点,并且1面向上。
算法分析 比较容易想到的算法是宽度优先搜索算法。但是,由于搜索树的枝树是3叉,所以,十分容易“超时”,即使加了剪枝条件效果仍旧不明显。
所以,我们对问题进行分析:对于一个棋子,其总共只有24种状态。在(1,1)点时,其向右翻滚至(2,1)点,向上翻滚至(1,2)点。而任意(I,J)点的状态是由(I-1,J)和(I,J-1)点状态推导出来的。
那么,如果规定棋子只能向上和向右翻滚,则可以用动态规划的方法将到达(M,N)点的所有可能的状态推导出来。而如果只能向上和向右翻滚能够到达(M,N)点的话,这种方法一定是最优的。
所以,我们从(1,1)开始进行宽度优先搜索,每扩展出一个节点变做一次动态规划,直到动态规划找到解,或者宽度优先搜索找到解为止,这样即可以保证最优解。
小结 动态规划算法某些时候去“帮助”搜索算法,或者搜索算法在某些时候去“帮助”动态规划,可以起到一个优势互补的作用,前者可以大大提高搜索算法的效率,而后者可以完成一些一般动态规划不能完成的任务。
4.3 标号法
标号法是一种图论算法,时间复杂度与动态规划几乎相等。让我们看一个例题:
例题7最短路径问题(2)
已知从A到J的路线及费用如图5,求从A到J的最短路线。
算法分析 本问题没有明显的阶段划分,且有后效性,不能按照例题1的办法来解决。
但是,从另一个角度来看,A到J的最短路径,它每一部分也是最优的,也就是说,本题也具有最优化性质。
后效性的问题怎么解决呢?我们尝试着重新划分阶段。如果我们每次都以某个状态为起点,遍历它的后继顶点,等所有已知状态都扩展完了,再来比较所有新状态,把值最小的那个状态确定下来,其它的不动。其实本题的隐含阶段就是以各结点的最优值的大小来划分的,而动态规划的过程就是按最优值从小到大前向动态规划。
人们习惯上把此题归入到图论中,并将这种方法称为标号法。
小结 标号法的本质就是特殊划分阶段的动态规划算法。而标号法的真正应用是在网络流当中。可见算法与算法之间互相关联。
综合举例
例题8杀人怪物
经过考察队员发现,在沙漠里的迷宫里竟然有很多从来没有见过的生物!这些生物异常的凶猛,喜爱吃动物的肉。听说“知己知彼,百战不殆”,于是,CarrierII在迷宫的入口处找啊找,终于发现了一张迷宫的地图,上面标有迷宫的详细情况以及怪物的位置、怪物的强弱。CarrierII每在迷宫中移动一步,要耗费1单位的时间。如果移动到的格子里有怪物,可以选择杀死,也可以选择不杀。杀死怪物需要耗费时间。怪物杀死后,该格会变为空地。可以重复地经过一个格子。问CarrierII怎样在限定的时间内杀最多的怪物?
输入格式:
第1行1个整数n(1<=n<=30),表示迷宫的大小为n*n。
第2行到第n+1行,每行n个整数,表示迷宫的状况。其中1为不能通过,0为可以通过。
第n+2行3个整数,表示CarrierII起始的位置(x,y)以及CarrierII所被限定的时间数。
第n+3行1个整数m(1<=m<=50),表示怪物的数量
第n+4行到第n+m+3行,每行3个整数,表示怪物的坐标以及杀死这个怪物所需要的时间。
输出格式:
2个整数,中间用一个空格隔开,第1个为杀死的怪物数,第2个为杀怪物所用的时间。
样例输入:
4 –迷宫大小
0 0 1 0
1 0 1 0
0 0 0 0
0 1 0 0
1 1 10 –人的坐标以及时间限制
3 –怪物数
1 2 3 –怪物坐标以及强弱
1 4 2
4 4 1
样例输出:
2 10
算法分析 题目很长,输入也很多,其实归结下来,无非也就是这几点:
(1)一张迷宫地图,1表示不可通,0表示可通。
(2)一些任务所需要的时间。
(3)开始的位置。
此题首先想到的方法是搜索。从迷宫的(x,y)点开始按照4个方向,直到找完所有路径为止。但是,这种搜索付出的代价是很大的,因为一个点有可能经过2次,或者以上。例如如下的表格:
0 0 0
怪物 起点 怪物
0 0 0
如果我们有足够充裕的时间杀掉所有的怪物的话,我们显然应该按照如下的步骤去走:(2,2)->(2,1)->(2,2)->(2,3)。
然后,我们想到把这个图转化成怪物到怪物之间的最短的距离。也就是建立一个邻接矩阵,矩阵中(i,j)位置存放了从第i个怪物走到第j个怪物的最短距离,然后再做搜索。这是一个很大的优化。主要是把浪费在走迷宫上的不必要的搜索给除去了。但是,由于最多有50个怪物,所以这个算法仍旧不能满足全部的需求。
仔细地想,这不就是刚刚所提到的标号法吗?
然后,我们用一个n2的循环去穷举每一对起点和终点,并且用上面的方法求出每一对顶点对之间的最小费用(因为“时间”的限制,我们要在动态规划的时候随时作判断,这可以多开一个元素数为怪物数的数组来解决)。整个算法的时间复杂度是O(n4)。这已经是一种次方级的算法。经过优化,我们可以并去一重循环,消耗一些空间存储中间状态,时间复杂度进一步降为O(n3),这已经是很优秀的了。