斐波那契數列是經過"遞歸"定義的,經過這個遞歸關係式,咱們能夠知道斐波那契數列中任意一個位置的數值。python
\[ \begin{equation}\begin{split} F(0) & = 0,\\ F(1) & = 1,\\ F(n) & = F(n-1) + F(n-2),\\ \end{split}\end{equation} \]數組
很容易地,咱們能寫出下面的代碼:緩存
def fib(n): if n == 0: return 0 if n == 1: return 1 return fib(n - 1) + fib(n - 2)
說明:ide
代碼自己用於計算是沒有問題的,可是仔細研究,咱們就會發現,咱們雖然使用遞歸實現了斐波那契數列在任意位置的值的計算,可是,若是要咱們本身計算的話,確定不會這樣計算,由於太耗時了。優化
耗時的緣由在於,在上述的遞歸實現中,存在大量的重複計算,例如:
要計算 fib(4),就得計算 fib(3) 和 fib(2),
要計算 fib(3),就得計算 fib(2) 和 fib(1),
此時 fib(2) 就被重複計算了,下面是一張圖,展現了部分重複計算的過程。idea
memo = None def _fib(n): if memo[n] != -1: return memo[n] if n == 0: return 0 if n == 1: return 1 memo[n] = _fib(n - 1) + _fib(n - 2) return memo[n] def fib(n): global memo memo = [-1] * (n + 1) return _fib(n)
這個版本是最接近咱們本身去計算斐波那契數列的第 \(n\) 項。想想的確是這樣,聰明的你必定不會遞歸去計算波那契數列的,由於咱們的腦容量是有限,不太適合作太深的遞歸思考,雖然計算機在行遞歸,可是咱們也沒有必要讓計算機作重複的遞歸工做。spa
def fib(n): memo = [-1] * (n + 1) memo[0] = 0 memo[1] = 1 for i in range(2, n + 1): memo[i] = memo[i - 1] + memo[i - 2] return memo[n]
針對一個遞歸問題,若是它呈樹形結構,而且出現了不少」重疊子問題」,會致使計算效率低下,「記憶化搜索」就是針對」重疊子問題」的一個解決方案,實際上就是」加緩存避免重複計算」。code
由上面的介紹咱們就能夠引出動態規劃的概念:遞歸
下面咱們給出「動態規劃」的官方定義:it
dynamic programming (also known as dynamic optimization) is a method for solving a complex problem by breaking it down into a collection of simpler subproblems, solving each of those subproblems just once, and storing their solutions – ideally, using a memory-based data structure.
將原問題拆解成若干子問題,同時保存子問題的答案,使得每一個子問題只求解一次,最終得到原問題的答案。
咱們一般的作法是:使用記憶化搜索的思路思考原問題,可是使用動態規劃的方法來實現。即「從上到下」思考,可是「從下到上」實現。
對於一個遞歸結構的問題,若是咱們在分析它的過程當中,發現了它有不少「重疊子問題」,雖然並不影響結果的正確性,可是咱們認爲大量的重複計算是不環保,不簡潔,不優雅,不高效的,所以,咱們必須將「重疊子問題」進行優化,優化的方法就是「加入緩存」,「加入緩存」的一個學術上的叫法就是「記憶化搜索」。
另外,咱們還發現,直接分析遞歸結構,是假設更小的子問題已經解決給出的實現,思考的路徑是「自頂向下」。但有的時候,「自底向上」的思考路徑每每更直接,這就是「動態規劃」,咱們是真正地解決了更小規模的問題,在處理更大規模的問題的時候,直接使用了更小規模問題的結果。