计算斐波那契数列中的项非常容易,因为实际上您只需要记住fib(n-2) 和fib(n-1) 即可计算fib(n)。因为它非常简单,所以任何算法都将非常简单,所以这个例子模糊了不同动态编程范式之间的细微差别。话虽如此,你提到的维基百科页面对斐波那契有很好的解释:https://en.wikipedia.org/wiki/Dynamic_programming#Fibonacci_sequence
如果函数在执行过程中调用自身,则称为递归函数。
可以使用或不使用递归来实现动态规划算法。
动态规划的核心是利用以下两个事实来编写算法:
- 问题的解决方案可以分解为子问题的解决方案;
- 当问题
P 的最优解S 分解为s1、s2、...到子问题p1、p2、...、然后s1 的解时, s2, ... 都是各自子问题的最优解。
请注意,这两个事实并非适用于所有问题。只有当这两个事实适用时,问题才适合动态规划。
一个简单的例子是找到从 A 点到 B 点的最短路径:如果从 A 到 B 的最短路径经过 C 点,那么它构成的从 A 到 C 和从 C 到 B 的两半也是最短路径。
在大多数情况下,您可以进行递归调用来解决子问题。但是“幼稚”的递归方法很容易导致指数算法,因为级联“为了解决这个问题,我需要解决这两个(或更多)子问题”,这可能会迅速增加您必须解决的问题数量解决。这是一个斐波那契的例子:
fib(5) = fib(4) + fib(3)
fib(4) = fib(3) + fib(2)
fib(3) = fib(2) + fib(1)
fib(2) = fib(1) + fib(0)
fib(1) = 1
fib(0) = 0
fib(1) = 1
fib(2) = fib(1) + fib(0)
fib(1) = 1
fib(0) = 0
fib(1) = 1
fib(3) = fib(2) + fib(1)
fib(2) = fib(1) + fib(0)
fib(1) = 1
fib(0) = 0
fib(1) = 1
在这里,我们必须计算 16 个词才能找到 fib(5)。但请注意,总共只有 6 个不同的术语。当然,通过避免一次又一次地重复相同的计算,我们可以提高效率。
为避免这种情况,动态规划算法通常相当于用子问题的解决方案填充数组。一旦您确定了子问题列表和数组,可能没有太多动力去“自上而下”进行递归调用,从最大的问题开始,然后依次将其分解为较小的子问题。相反,您可以从最琐碎的问题开始“自下而上”地填充数组,然后使用这些问题来解决更复杂的问题,直到您解决了最初想要解决的问题。在斐波那契数列的情况下,您可能会得到以下代码:
int f[n+1];
f[0] = 0;
f[1] = 1;
for (int k = 2; k <= n; k++)
f[k] = f[k-2] + f[k-1];
return f[n];
但是,在斐波那契数列的情况下,您只需要随时记住最后两个术语,因此无需使用从 fib(0) 到 fib(n) 的所有术语填充整个数组,您只需保留两个变量(或大小为 2 的数组)与前两个结果。可以说这仍然是动态编程,尽管它最终只是一个循环,按顺序计算序列的项,而且很难看到任何关于它的“动态”。