上世纪40年代,RichardBellman最早使用动态规划这一概念表述通过遍历寻找最优决策解问题的求解过程。1953年,RichardBellman将动态规划赋予现代意义,该领域被IEEE纳入系统分析和工程中。为纪念Bellman的贡献,动态规划的核心方程被命名为贝尔曼方程,该方程以递归形式重申了一个优化问题。
在“动态规划”(dynamicprogramming)一词中,programming与“计算机编程”(computerprogramming)中的programming并无关联,而是来自“数学规划”(mathematicalprogramming),也称优化。因此,规划是指对生成活动的优化策略。举个例子,编制一场展览的日程可称为规划。在此意义上,规划意味着找到一个可行的活动计划。概述
图1使用最优子结构寻找最短路径:直线表示边,波状线表示两顶点间的最短路径(路径中其他节点未显示);粗线表示从起点到终点的最短路径。
不难看出,start到goal的最短路径由start的相邻节点到goal的最短路径及start到其相邻节点的成本决定。
最优子结构即可用来寻找整个问题最优解的子问题的最优解。举例来说,寻找图上某顶点到终点的最短路径,可先计算该顶点所有相邻顶点至终点的最短路径,然后以此来选择最佳整体路径,如图1所示。
一般而言,最优子结构通过如下三个步骤解决问题:
a)将问题分解成较小的子问题;
b)通过递归使用这三个步骤求出子问题的最优解;
c)使用这些最优解构造初始问题的最优解。
子问题的求解是通过不断划分为更小的子问题实现的,直至我们可以在常数时间内求解。
图2Fibonacci序列的子问题示意图:使用有向无环图(DAG,directedacyclicgraph)而非树表示重复子问题的分解。
为什么是DAG而不是树呢?答案就是,如果是树的话,会有很多重复计算,下面有相关的解释。
一个问题可划分为重复子问题是指通过相同的子问题可以解决不同的较大问题。例如,在Fibonacci序列中,F3=F1+F2和F4=F2+F3都包含计算F2。由于计算F5需要计算F3和F4,一个比较笨的计算F5的方法可能会重复计算F2两次甚至两次以上。这一点对所有重复子问题都适用:愚蠢的做法可能会为重复计算已经解决的最优子问题的解而浪费时间。
为避免重复计算,可将已经得到的子问题的解保存起来,当我们要解决相同的子问题时,重用即可。该方法即所谓的缓存(memoization,而不是存储memorization,虽然这个词亦适合,姑且这么叫吧,这个单词太难翻译了,简直就是可意会不可言传,其意义是没计算过则计算,计算过则保存)。当我们确信将不会再需要某一解时,可以将其抛弃,以节省空间。在某些情况下,我们甚至可以提前计算出那些将来会用到的子问题的解。
总括而言,动态规划利用:
1)重复子问题
2)最优子结构
3)缓存
动态规划通常采用以下两种方式中的一种两个办法:
自顶向下:将问题划分为若干子问题,求解这些子问题并保存结果以免重复计算。该方法将递归和缓存结合在一起。
自下而上:先行求解所有可能用到的子问题,然后用其构造更大问题的解。该方法在节省堆栈空间和减少函数调用数量上略有优势,但有时想找出给定问题的所有子问题并不那么直观。
为了提高按名传递(call-by-name,这一机制与按需传递call-by-need相关,复习一下参数传递的各种规则吧,简单说一下,按名传递允许改变实参值)的效率,一些编程语言将函数的返回值“自动”缓存在函数的特定参数集合中。一些语言将这一特性尽可能简化(如Scheme、CommonLisp和Perl)