从 Fibonacci 数列看“动态规划”思想
本文内容
- 概述
- 从 Fibonacci 数列看“动态规划”思想
- 动态规划基础
- 动态规划步骤
- 动态规划意义
- 动态规划应用
- 备注
概述
在数据结构中,最经典的算法/问题是:Floyd 算法(最短路径)、哈夫曼编码和 Fibonacci(斐波那契数列),背包问题等等。但当时,这些经典仅仅是描述了一个问题的解决方法,没有对整个这类问题更深入的阐述。
而事实上,随着对问题理解深入,发现这些算法和问题都包含了“动态规划”的思想。在此思想基础上,对这些算法和问题可以进行重大改进——算法更简单、时间复杂度更小。
“动态规划(Dynamic Programming,DP)”对每个子问题只求解一次,将其结果保存在一张表中,从而避免每次遇到各个子问题时重新计算。“Programming”是指一种规划,在这里以及线性规划中,都是指使用一种表格化的解法,而不是指写计算机代码。
从 Fibonacci 数列看“动态规划”思想
下面用 C# 演示斐波那契数列的一般递归算法,以及利用“动态规划”思想的改进算法。
using System;
namespace Fibonacci
{
class Program
{
const int N = 6;
static int[] m = new int[N] { 1, 1, 0, 0, 0, 0 };
static void Main(string[] args)
{
Console.WriteLine("Fibonacci(5) = " + Fibonacci(5));
Console.WriteLine("Fibonacci(5) with Dynamic Programming = " + Fibonacci_DP(5, ref m));
Console.ReadKey();
}
/// <summary>
/// 时间复杂度 O(n!)
/// </summary>
/// <param name="n"></param>
/// <returns></returns>
static int Fibonacci(int n)
{
if (n == 0 || n == 1)
return 1;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
/// <summary>
/// 时间复杂度 O(n)
/// </summary>
/// <param name="n"></param>
/// <param name="m"></param>
/// <returns></returns>
static int Fibonacci_DP(int n, ref int[] m)
{
if (m[n] == 0)
m[n] = Fibonacci_DP(n - 1, ref m) + Fibonacci_DP(n - 2, ref m);
return m[n];
}
}
}
其中,Fibonacci 方法是斐波那契数列一般的递归算法。而 Fibonacci_DP 方法是利用“动态规划”思想的算法。
从时间复杂度上看,一般的递归算法是 O(n!),呈指数级增长。而采用“动态规划”思想的算法只有 O(n)。
当 n=5 时,Fibonacci(5) 的计算过程如下:
fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
可以看出,这个递归算法,对每个子问题都要重新计算。而实际上,若利用“动态规划”思想这是没必要的。对于已经计算完的子问题,下次再遇到直接使用。将已经计算的结果保存在数组 m 中,在后面直接使用,避免重复计算。
在实际开发中,了解一些算法方面的知识,往往很有必要。能够分析自己的算法,比如你的算法在何种输入规模,效率是较高的。
个人觉得,在开发 ERP 系统时,会涉及很多算法。最近写一个关于改变工厂生产过程中变更加工工艺(客户称为“工艺再造”)的递归算法,猛然发现,满复杂的。当时有点惊讶。之前,除了上学时《数据结构》里的经典算法和典型问题外,思考得并不多。
下面是大概说明一下“动态规划”思想。
动态规划基础
采用“动态规划”方法解决最优化问题中的两个要素:最优子结构和重叠子问题。
- 最优子结构。子问题的最优解是问题的一个最优解。当一个问题具有最优子结构时,提示我们动态规划可能会适用。
- 子问题重叠。用来解原问题的递归算法可反复地解同样的子问题,而不是总在产生新的子问题。
动态规划步骤
动态规划思想可分为如下 4 个步骤:
- 描述最优解的结构。
- 递归定义最优解的值。
- 按自底向上的方式计算最优解的值。
- 由计算出的结果构造一个最优解。
第 1~3 步构成问题的动态规划解的基础。第4步在只要求计算最优解的值时可以略去。如果的确要做第 4 步,则有时要在第 3 步的计算中记录一些附加信息,使构造一个最优解变得容易。
动态规划意义
“动态规划”是运筹学的一个分支,是求解决策过程(Decision Process)最优化的数学方法。20 世纪 50 年代初,美国数学家 R.E.Bellman 等人在研究多阶段决策过程(Multistep Decision Process)的优化问题时,提出了著名的最优化原理(Principle of Optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957 年出版了他的名著《Dynamic Programming》,这是该领域的第一本著作。
动态规划在经济管理、生产调度、工程技术和最优控制等方面得到了广泛的应用。
虽然,动态规划主要用于求解以时间划分阶段的动态过程的优化问题,但一些与时间无关的静态规划,如线性/非线性规划,只要人为地引进时间因素,视为多阶段决策过程,也可以用动态规划方法求解。
动态规划方法是解最优化问题的一种途径和方法,而不是一个特定算法。针对一种最优化问题,由于各种问题的性质不同,往往确定最优解的条件也不相同。也就不存在一种万能的动态规划算法。因此,必须具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。
动态规划应用
- 最优二叉查找树——如将一篇文章从英文翻译为法语,我们需要一棵最优二叉查找树。
- 最长公共子序列(LCS)——用来比较生物的 DNA 序列。
- Viterbi 算法——用动态规划做语音识别。
- 矩阵链乘法——多个矩阵相乘时,相乘的顺序对效率影响很大。
备注
- R.Bellman 在 1955 年开始系统地研究动态规划。虽然在之前已经知道最优化技术含有动态规划的元素,Bellman 给这个领域提供了坚实的数学基础。
- Hu 和 Shing 给出了矩阵链乘法的一个 O(nlgn) 时间的算法。
- 最长公共子序列问题的 O(mn) 时间算法是一个一般的算法。 Knuth 提出了 LCS 问题的次二次方的算法是否存在的问题。Masek 和 Paterson 给出一个在 O(mn/lgn) 时间内执行的算法来肯定地回答这个问题,其中 n<=m 而且此序列是从一个有界集合中而来。在输入序列中没有元素出现超过一次的特殊情况中,Szymanski 说明这个问题可在 O((n+m)lg(n+m)) 时间内解决。这些结果中偶许多延伸到了计算字符串编辑距离的问题上。
- 一篇由 Gilbert 和 moore 撰写的关于可变长二元编码的早期论文有这样的应用:在所有的概率 pi 都是 0 的情况下构造最优二叉查找树;这篇论文一个 O(n^3) 时间的算法。Aho、Hopcroft 和 Ullman 给出了该算法。Hu 和 Tucker 设计了一个算法,它在所有的概率 pi 都是 0 的情况下,使用 O(n^2) 的时间和 O(n) 的空间;随后,Knuth 把时间降到 O(nlgn)。