1、动态规划的深入讨论 东北育才学校 李刚 【 关键字 】 动态规划、状态 【 摘要 】 本文讨论了一种解决问题十分有效的技术 “动态规划”。它较高的解题效率一直受到很大的关注。本文首先对“动态规划”的理论基础进行了讨论。给出了一个用“动态规划”可以解决的问题的两个先决条件:“最优子结构”与“无后效性”。接着,讨论了在实际应用中的两个比较常见的问题:“动态规划”中状态的选定与存储。再通过以上问题的讨论,引出了“动态规划”的基本思维方法:“不做 已经做过 的工作”以及“动态规划”技术在解决问题中速度惊人的原因 “解决了查看中的冗余,达到了速度的极限”。最后,阐述了解决“动态规划”问题的一般步骤,即“
2、思考,计划,应用” 【 正文 】 一 引 论 在信息学竞赛中,特别是最近几年,“动态规划”作为一种解题工具,经常被提及。其应用范围愈来愈广,应用程度也愈来愈深。那么,“动态规划”究竟与其它的算法有什么差别?它有什 么具体的应用价值呢?本文将对此进行讨论。 我们先通过一个具体问题认识一下“动态规划”。 例 1 :图 1 中给出了一个地图,地图中每个顶点代表一个城市,两个城市间的连线代表道路,连线上的数值代表道路的长度。现在,我们想从城市 A到达城市 E,怎样走路程最短,最短路程的长度是多少? 假设 : DisX为城市 X到 E的最短路线的长度;( X表示任意一个城市) MapI,J表示 I, J
3、两个城市间的距离,若 MapI, J=0,则两个城市不连通。 这个问题我们可以用搜索法来做,程序很容易写出来: Var Se:未访问的城市集合; Function Long(Who:当前访问城市 ):Integer; :求当前访问城市与城市E的最短距离。 Begin If Who=E Then Search:=0 Else Begin Min:=Maxint; For I取遍所有城市 Do If ( MapWho,I0) And (I In Se) Then Begin Se:=Se-I; J:=MapWho,I+Long(I); Se:=Se+I; If JMin Then Min:=J;
4、End; Long:=Min; End; End; Begin Se:=除 A外所有城市的集合; DisA:=Long(A); End. 这个程序的效率如何呢?我们可以看到,每次除了已经访问过的城市外,其他城市都要访问,所以时间复杂度为 )!(NO ,这是一个“指数级”的算法,那么,还有没有更好的算法呢? 首先,我们来观察一下这个算法。在求从 B1 到 E的最短路径的时候,先求出从 C2到 E的最短路径;而在求从 B2 到 E的最短路径的时候,又求了一遍从 C2到 E的最短路径。也就是说,从 C2到 E的最短路径我们求了两遍。同样可以发现,在求从 C1、 C2到 E的最短路径的过程中,从 D1
5、 到 E的最短路径也被求了两遍。而在整个程序中,从 D1 到 E的最短路径被求了四遍, 这是多么大的一个浪费啊! 如果在求解的过程中,同时将求得的最短路径的距离“记录在案”,随时调用, 那会是多么的方便啊! 于是,一个新的思路诞生了,即: 由后往前 依次推出每个 Dis值,直到推出 DisA为止。这个思路的确很好,但等等,究竟什么是“由后往前”呢? 所谓“后”、“前”是我们自己为城市编的序号,当两个城市 I, J 的前后顺序定为 I“前” J“后”时,必须满足这个条件: 或者 I, J 不连通,或者 DisI+MapI,J DisJ。 因为如果 I, J 连通且 DisI+MapI,JDisJ
6、,则说明 DisJ存在更优的情况,可 J位于 I后,就不可能推出此情况,会影响最后的解。那么,我们如何划分先后次序呢? 如图 2 所示。我 用不同颜色给城市分阶段,可以用 阶段 表示每个城市的次序,因为 阶段 的划分有如下性质: 1:阶段 I的取值只与阶段 I+1 有关,阶段 I的取值只对阶段 I-1的取值产生影响; 2:每个阶段的顺序是确定的,不可以调换任两个阶段顺序; 通过这两个性质,可以推出阶段作为“前”、“后”顺序满足刚才提出的条件,所以我们可以用 阶段 作为每个城市的次序,然后从阶段 3 倒推至阶段1,再推出 DisA。 公式: DisX=Min DisY: Y是下一个阶段中与 X相
7、连通的城市 注:可以把 E 看成第 4个阶段, A 看成第 0 个阶段。 程序: DisE=0 For X=阶段 3的 每个城市 Downto 阶段 0 的每个城市 Do Begin DisX:=Maxint; For Y=阶段 X的下一个阶段中的每个城市 Do If DisY+MapX,YDisX Then DisX:=DisY+MapX,Y; End。 这个程序的时间复杂为 )( 2NO ,比上一个程序的复杂度 )!(NO 要小得多。第二个算法就是“动态规划”算法。 二 动态规划的理论基础 通过上面的例子,我们对“动态规划”有了一个初步认识,它所处理的问题是一个“多阶段决策问题”。我们现在
8、对一些概念进行具体定义: 状态 ( State):它表示事物的性质,是描述“动态规划”中的“单元”的量。如例 1中的城市 A, B1, D2, E都为单个的状态。 阶段 ( Stage):阶段是一些性质相近,可以 同时处理 的状态集合。通常,一个问题可以由处理的先后次序划分为几个阶段。如例 1 中的问题由三个阶段组成,其中阶段 1 包含状态 B1, B2。实际上,阶段只是标识那些处理方法相同、处理顺序无关的状态。一个阶段既可以包含多个状态,也可以只包含一个状态。其关系很类似分子与原子的关系。 状态转移方程: 是 前一个阶段的状态转移到后一个的状态的演变规律,是关于两个相邻阶段状态的方程,是“动
9、态规划”的中心。 决策 ( Decision):每个阶段做出的某种选择性的行动。它是我们程序所需要完成的选择。 动态规划所处理的问题是一个“ 多阶段决策问题 ”,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图 3所示。 总体上来说,一个“动态规划”算法应该包含上面提到的 几种基本关系与属性。 那么,什么样的“决策问题”才可以划分阶段,来用“动态规划”求解呢? 我们说一个“决策问题”只有具有以下性质时,才可以考虑划分阶段,用“动态规划”算法: 一:最优子结构 即一个问题的最优解只取决于其
10、子问题的最优解,也就是说,非最优解对问题的求解没有影响。在例 1 中,如果我们想求 DisB1的最小值,我们只需要知道 C1, C2, C3 的最小值,而不用考虑其他的路径,因为其他的非最优路径肯定对 DisB1的结果没有影响。我们再来看一个问题: 例 2有 4 个点,分别是 A、 B、 C、 D,如图 4 所示,相邻两点用两条连线C2k,C2k-1(1 k 3)表示两条通行的道路。连线上方的数字表示道路的长度。我们定义从 A 到 D 的所有路径中,长度除以 4 所得余数最小的路径为最优路径。求一条最优路径。 在这个题目中,我们如果还按照刚才的方法来求解就会发生错误。例如,按照例 1 的思维,
11、 A 最优取值可以由 B 的最优取值来确定,而 B 的最优取值为0,所以 A的最优值为 2,而实际上,路径 C1 C3 C5 可得最优值为 0,所以,B的最优路径并不是 A 最优路径的子路径,也就是说, A 的最优取值 不是由 B的最优取值决定的,其不具有最优子结构。 由此可见,并不是所有的“决策问题”都可以用“动态规划”来解决。所以, 只有当一个问题呈现出最优子结构时,“动态规划”才可能是一个合适的侯选方法。 二:无后效性 即一个问题被划分阶段后,阶段 I 中的状态只能由阶段 I+1 中的状态通过状态转移方程得来,与其他状态没有关系,特别是与未发生的状态没有关系,这就是无后效性。实际上,如果
12、我们把这个问题中的状态定义成图中的顶点,两个状态之间的转移定义为边,转移过程中的权值增量定义为边的权值,则这个问题实际上就是在一个 “ 有向无环加权图” 中寻找两个顶点路径的问题。因为无后效性,所以没有环路(否则,无论如何划分阶段,都可以出现后效性)。即这个图可以进行“拓扑排序”,那么,我们至少可以以他们拓扑排序的顺序划分阶段。 我们来看一个具体例子: 例 3:欧几里德货郎担问题是对平面给定的 n 个点确定一条连结各点的、闭合的游历路线问题。图 5(a)给出了七个点问题的解。 Bitonic 旅行路线问题是欧几里德货郎担问题的简化,这种旅行路线先从最左边开始,严格地由左至右到最右边的点,然后再
13、严格地由右至左到出发点,求路程 最短的路径长度。图 5( b)给出了七个点问题的解。 这两个问题看起来很相似。但实质上是不同的。为了方便讨论,我将每个顶点标记了号码。由于必然经过最右边的顶点 7,所以一条路 (P1 P2)可以看成两条路( P1 7)与 (P2 7)的结合。所以,这个题目的状态可以用两条道路结合的形式表示。我们可以把这些状态中,两条路中起始顶点相同的状态归于一个阶段,设为阶段 P1,P2。 那么,对于 Bitonic 旅行路线问题来说,阶段 P1,P2如果可以由阶段Q1,Q2推出,则必须满足的条件就是 :P1Q1 或 P2Q2。例如,阶段 3,4中的道路可以由阶段 3,5中的道
14、路加一条边 4 5 得出,而阶段 3,5的状态却无法由阶段 3,4中的状态得出,因为 Bitonic 旅行路线要求必须严格地由左到右来旅行。所以如果我们已经知道了阶段 3,4中的状态,则阶段 3,5中的状态必然已知。因此我们可以说, Bitonic 问题满足“无后效性原则”,可以用“动态规划”算法来解决,其程序可以参见 Pro_3_1.Pas。 对于欧几里德货郎担问题 ,阶段与阶段之间没有什么必然的“顺序”。如道路 3 2 5 7,4 6 7属于阶段 3,4,可由属于阶段 2,4的道路 2 57, 4 6 7推出;而道路 2 3 6 7, 4 5 7属于阶段 2,4,可由属于阶段 3,4的道路
15、 3 6 7, 4 5 7推出。如果以顶点表示阶段,推出关系表示边,那么,阶段 3,4与阶段 2,4对应的关系就如图 6 所示。 我们可以很清晰地看出,这两个阶段的关系是“有后效性”的。因为这个图中存在“环路”。对于这个问题是不能像上一个问题那样来解决的,事实上,这个问题是一个 NP完全问题,其解决的时间复杂度很可能是指数级的。 所以,对 于一个问题能否用“动态规划”来解决的一个十分关键的判断条件就是“它是否有后效性”。而我们在判断这个问题是否有“后效性”时,一个很有效的方法就是将这个问题的阶段作为顶点,阶段与阶段之间的关系看作有向边,判断这个有向图是否为“有向无环图”,亦即这个图是否可以进行
16、“拓扑排序”。 通过上面的说明,我们可以总结出一些解决 “动态规划”问题 的基本方法与步骤: 1:确定问题的研究对象,即确定状态。 2:划分阶段,确定阶段之间的状态转移方程。 3:考察此问题现在可否用“动态规划”来解决: :考察此问题是 否具有“最优子结构”。 :考察此问题是否为“无后效性”。 4:如果发现此问题目前不能用“动态规划”来解决,则应该调整相应的定义与划分,以达到可以用“动态规划”来解决。 以上只是一般情况下的“动态规划”思维过程。一些较为简单的问题可以“按部就班”来操作,但大多数的“动态规划”问题,特别是作为信息学竞赛中的“动态规划”问题,考察的知识是多方面的,应用的技巧是灵活多
17、变的。下面,对“动态规划”在应用中的一些重点、难点进行讨论。 三 动态规划的实际应 用 我们衡量一个算法的标准,无外乎时间、空间两项指标。“动态规划”算法的时间大多数为“多项式级”的,比起同样解决这个问题的搜索算法“指数级”的时间来说,“动态规划”的时间需要是很少的,所以我们在实际应用中,很少考虑“动态规划”算法的时间问题,而最经常考虑的是空间问题:即状态的选定与存储。 1:状态的选定: 对于一个“动态规划”算法来说,阶段的划分显得不很重要,因为阶段只是一些可以等同处理的状态的集合,我们尽可以把单个的状态定义为一个阶段。所以,状态的选定对整个问题的 处理起了决定性的作用。我们选定的状态必须满足
18、如下两点: 1:状态必须完全描述出事物的性质,两个不同事物的状态是不同的; 2:必须存在状态与状态之间的“转移方程”。以便我们可以由“初始问题”对应的状态逐渐转化为“终结问题”对应的状态。 状态是描述事物性质的量,所以我们应该以这个标准,根据题目中的具体要求来具体分析,我们来看下面一个例子: 例 4有一个奶牛运输公司,需在 7 个农场 A,B,C,D,E,F,G 之间运输奶牛,从 A 出发,完成任务后要回到 A。运输车 每次只能运送一头奶牛,每个运输任务是由一对字母给出的,分别表示奶牛从哪个农场被运向另外一个农场。任务总数为 N(1 N 12)。已知这些农场之间的距离,求出一条完成所有任务的最
19、短路线。 我们先分析这个问题的解决方法:我们的目的是完成一些任务,由于运输车每次只能运送一头奶牛,所以我们只能一个接一个地处理这些任务。由于题目求的是一条完成这些任务的最短路线,所以在完成任务的过程中,我们走的路线都是最短路线,也就是说,一旦我们确定了完成任务的顺序 P1 P2 PN,那么这条路线的最短时间也就确定了。我们的目的就是 确定这 N 个任务的完成顺序。所以,这个问题的研究对象就是某些任务的集合 S。那么,状态该如何定义呢?既然研究对象是集合,所以很自然地就想起了描述集合中元素关系的方法。我们可以把状态定义为一个数组( Nttt ,., 21 ) ,每一个it 或者为 0,表示任务
20、i 不在当前集合中;或者为 1,表示任务 i 在当前集合中。但如果这样定义状态,我们会发现,无法写出状态转移方程,因为涉及到前后两个任务的“接口”的最短路线长度。所以我们还需要一个量 x 来限定当 前 这 个 任 务 集 合 中 首 先 要 执 行 任 务 , 则 一 个 状 态 定 义 为( Ntttx ,., 21 ) ,其中 xt 必为 1。那么,一个状态对应的权值就是完成这个状态描述的任务集合中任务的最短路线长度。则状态转移方程为: ( Ntttx ,., 21 ) = ),(), . . . . . . ,( 21 yxD i spppyM i n n 其中, y 是不等于 x 且
21、yt 1 的值,若 i x,则 ii tp ; 0xp 。假设Dis(x,y)为由任务 x 的终点城市到任务 y 的起点城市的最短距离。这个转移方程的初始条件为: ( Ntttx ,., 21 ) =任务 x 的终点城市到 A 的距离 ,其中 1xt ,其余的 it 均为 0。 在实际处理中,用这样一个最大可能为 12 维的数组显然是不方便的,既然每一位只能是 0 或 1,所以我就把( Nttt ,., 21 )看作一个十进制数的二进制表示,于 是,状态 数组为一个 二维数 组 ,具体程 序 见附录中的Pro_4_1.Pas。 从这个问题中我们可以看出,一个“动态规划”问题的状态选定实质上就是
22、选择描述这个问题中事物的最贴切,最简洁的方法。状态的选定不是一蹴而就的,而是一个在思考过程中逐步调整,逐步完善的过程。 2:状态的存储 当状态选定后,我们面对的问题就是如何存储 状态了。从理论上讲,每一个状态都应该存储两个值,一个是此状态的最 优的权值,一个是决策标识值。但实际中,我们面对的“动态规划”问题很多 都是状态数目十分庞大,这就要求我们在存储上必须做一定的优化才可以实现 这个算法的程序化。主要 的优化就是: 舍弃一切不必要的存储量。 在一些问题中,题目给出了只在某一范围内的 关系,所以我们只需存储这一范围内的状态即可。下面来看这个问题: 例 5一个生物体的结构可以用“基元”的序列表
23、示,一个“基元”用一些英文字符串表示。对于一个基元集合 P,可以将字符串 S 作为 N 个基元P1,P2PN 的依次连接而成。问题是给定一个字符串 S 和一个基元集合 P,使 S的前缀可由 P 中的基元组成。求这个前缀的最大长度。基元 的长度最大为 20,字符串的长度最大为 500,000。例如基元集合为 A,AB,BBC,CA,BA,字符串为ABABACABAABCB,则最大长度为 11,具体组成见图 7。图中不同的“基元”以不同的颜色标出。 这个问题的状态十分容易确定,字符串中的每一位的性质只有两种,即:包含这一位字符的字符串前缀是否可以由基元序列构成。那么,我们就可以设一个数组 : Co
24、uld: Array1.Max Of Boolean作为表示字符串中每一位的状态。那么,状态转移方程为: CouldK:=CouldK Or (CouldK-W And (SK-W+1.K=JiI) 其中, Couldk的初始值均为 False, JiI表示第 I 个基元对应的字符串,其长度为 W, SK-W+1.K为题中给定的字符串从 K-W+1 位到 K 位的字符组成的字符串。 写到这里,这个问题似乎已经解决了,我们只需求出使 CouldI为 True 最大的 I 即可。但只要回过头看一看问题就会发现,字符串最长为 500,000,根本无法存储,该如何是好呢?如果再仔细观察一遍转移方程,就
25、会发现: CouldI只与 CouldI-W发生关系,而 W为基元的长度,基元的长度最大为 20,也就是说,我们如果想推出 CouldI,最多只需要知道 CouldI-20至 CouldI-1的值即可,其它的 Could 值我们可以不去管它。这样,我们需要记录的 Could 值一下子就由 500000 减少到 20,这个突变是巨大的。具体程序见 Pro_5.Pas。 这个问题为什么只记录很少的状态就可以呢?这是因为它特殊的转移方程与求解顺序。求解顺序是按照字符串中位置的先后而定的,而转移方程中,当前状态只与比它先求解的那 20 个状态发生关系,所以这个问题可以如此解决。 但对于大多数问题来说,
26、这样“美丽”的“结构”不一定具备,那么我们在解决这样的问题时,只有“ 处心积虑 ”地削减存 储量。我们来看一个例子。 例 6某公司运进一批箱子,总数为 N( 1 N 1000) 由“传送带”依次运入,然后在仓库内至多排成 P( 1 P 4)列,(如图 8 所示)。 现已知运来的箱子最多为 M( 1 M 20)种,想把同一种类的箱子尽量排在一起,以便美观。“美观程度” T定义为: T=(每列依次看到的不同种类数); 所谓“依次看到的不同种类数”即为:如果某一列中第 K 个箱子与第 K-1个箱子种类不同,则“美观程度”的值加 1。求一种调动安排,使各列的“美观程度”值的和最小。 这个问题的思路如下
27、:第 K 个箱子 需要选择一列来放,如果它放在与其种类不同的某个箱子上,则它会使总的“美观程度”值加 1。也就是说,第 K 个箱子的摆放只与当前 P 列的顶端情况有关。又如果想求这个箱子摆放后的最小值,自然需要这个前( K-1)个箱子在此顶端情况时的最小值。这个多阶段决策问题同时满足最优化原理与无后效性原则,所以这个问题可以用“动态规划”的方法来解决。则状态的选定很明显,为( k, P1, P2, P3, P4), K 是当前即将放入的箱子的编号, P1 P4放入第 K 个箱子后各列的“顶端情况”,即放入第 K 个箱子后顶端是什么种类的箱子(假设当前有 4列)。 则阶段划分也很明显,以箱子由传
28、送带运来的先后顺序划分阶段。那么,状态转移方程就是: ( K,P1,P2,P3,P4) =Min(K-1,Q1,Q2,Q3,Q4) +Dis 其中, Q1 Q4为未放入第 K 个箱子前各列的情况, Dis为 0 或 1,取决与第 K 个箱子与其列中上一个箱子的种类有无差别。 写到这里,“动态规划”的算法各个步骤已经清楚了,似乎可以直接写程序了。但对于本题来说,存储大量的信息,是我们所不得不面对的一个问题。Pascal语言所允许的最大可用空间为 640KB,在保护模式下,最多也不过 1000多 KB。一个“动态规划”程序所需存储的一般有两个值,一是决策信息,二是状态的价值。对于本题,若按普通的方
29、法,最多时为 1000 203 (1 2)字节 ,根本无法存储。必须对存储做一定的优化,舍弃一切不必要的量: 1 只记录每步的“决策信息”,不记录每步的状态的价值,这样可省下 32的空间; 2 对每步的状态来说,所对应的 P 列顶端不含有两个同一种的箱子; 3 除本次要选的列以外,其它( P-1)列顶端的排列对结果没影响,所以存 储的状态应为另外( P-1)列的组合,而非排列。 这样,总的存储量可由 24000KB减少到 1200KB,缩小了约 20 倍,在Pascal保护模式下完全可以操作。具体的程序见 Pro_6.Pas。 通过以上两个问题,我们可以看出,“动态规划”在实际应用中,速度几乎
30、“不成问题”,但存储状态的“环节”却必须思考再思考,优化再优化。近几年的试题中,大多数“动态规划”问题的难点与重点就是“动态规划”中状态的选定与存储。这也是“动态规划”在实际应用中的重中之重。 四动态规划的深入思考 以上讨论了“动 态规划”的理论基础与在实际应用中可能遇到的问题。那么,“动态规划”究竟好在哪里?我们为什么要用“动态规划”呢?从上面的一些例子可以看出,“动态规划”这种方法最大优点就是节约了时间。这个“巨大的优点”可以从例 1 的解决中看出。在实际中,搜索算法与“动态规划”算法的时间差异是巨大的,表 1是例 1的两个算法对应程序的执行时间。 测试数据 算法 1 2 3 4 5 搜索算法 1.04s 1.42s 1.05s 7.42s 22.92s 动态规划算法 0.01s 0.01s 0.06s 0.02s 0.01s (表 1)