动态规划算法通常用于求解具有某种最优性质的问题。在数学与计算机科学领域,动态规划用于解决那些可分解为重复子问题(overlapping subproblems,想想递归求阶乘吧)并具有最优子结构(optimal substructure,想想最短路径算法)的问题,动态规划比通常算法花费更少时间。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
动态规划的应用十分广泛,如最大子序列和、最优二叉查找树、斐波那契数列等等。
2. 斐波那契数列
寻找Fibonacci序列中第n个数,基于其数学定义的直接实现:
function fib(n)
if n = 0
return 0
else if n = 1
return 1
return fib(n-1) + fib(n-2)
如果我们调用fib(5),将产生一棵对于同一值重复计算多次的调用树:
fib(5)特别是,fib(2)计算了3次。在更大规模的例子中,还有更多fib的值被重复计算,将消耗指数级时间。
现在,假设我们有一个简单的映射(map)对象m,为每一个计算过的fib及其返回值建立映射,修改上面的函数fib,使用并不断更新m。新的函数将只需O(n)的时间,而非指数时间:
var m := map(0 → 1, 1 → 1)
function fib(n)
if map m does not contain key n
m[n] := fib(n-1) + fib(n-2)
return m[n]
这一保存已计算出的数值的技术即被称为缓存,这儿使用的是自顶向下的方法:先将问题划分为若干子问题,然后计算和存储值。
在自下而上的方法中,我们先计算较小的fib,然后基于其计算更大的fib。这种方法也只花费线性(O(n))时间,因为它包含一个n-1次的循环。然而,这一方法只需要常数(O(1))的空间,相反,自顶向下的方法则需要O(n)的空间来储存映射关系。
function fib(n)
var previousFib := 0, currentFib := 1
if n = 0
return 0
else if n = 1
return 1
repeat n-1 times
var newFib := previousFib + currentFib
previousFib := currentFib
currentFib := newFib
return currentFib
在这两个例子,我们都只计算fib(2)一次,然后用它来计算fib(3)和fib(4),而不是每次都重新计算。
3. 货币找零问题
见:货币找零问题
未完待续...