動態規划(1)使用斐波那契數列引入了動態規划的概念


9-1 使用斐波那契數列引入了動態規划的概念

一、計算斐波那契數列的第 \(n\) 項數值

1、斐波那契數列的定義

斐波那契數列是通過"遞歸"定義的,通過這個遞歸關系式,我們可以知道斐波那契數列中任意一個位置的數值。

\[\begin{equation}\begin{split} F(0) & = 0,\\ F(1) & = 1,\\ F(n) & = F(n-1) + F(n-2),\\ \end{split}\end{equation} \]

2、第 1 版 Python 代碼實現:使用斐波那契數列的定義式子遞歸實現

很容易地,我們能寫出下面的代碼:

def fib(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    return fib(n - 1) + fib(n - 2)

說明:

  1. 代碼本身用於計算是沒有問題的,但是仔細研究,我們就會發現,我們雖然使用遞歸實現了斐波那契數列在任意位置的值的計算,但是,如果要我們自己計算的話,肯定不會這樣計算,因為太耗時了。

  2. 耗時的原因在於,在上述的遞歸實現中,存在大量的重復計算,例如:
    要計算 fib(4),就得計算 fib(3) 和 fib(2),
    要計算 fib(3),就得計算 fib(2) 和 fib(1),
    此時 fib(2) 就被重復計算了,下面是一張圖,展示了部分重復計算的過程。

  1. 要解決上一步的問題,就要避免重復計算,我們可以引入一個 memo 數組,用於存入已經計算過一次的 fib 的值,下一次需要這個值的時候,再從中取,下面是代碼實現。

3、第 2 版 Python 代碼實現:加入了記憶化搜索,即使用了緩存數組,以避免重復計算

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)

4、第 3 版 Python 代碼實現:雖然很簡單,但是我們就可以稱之為“動態規划”的解法

這個版本是最接近我們自己去計算斐波那契數列的第 \(n\) 項。想一想的確是這樣,聰明的你一定不會遞歸去計算波那契數列的,因為我們的腦容量是有限,不太適合做太深的遞歸思考,雖然計算機在行遞歸,但是我們也沒有必要讓計算機做重復的遞歸工作。

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]

二、什么是“記憶化搜索”

針對一個遞歸問題,如果它呈樹形結構,並且出現了很多”重疊子問題”,會導致計算效率低下,“記憶化搜索”就是針對”重疊子問題”的一個解決方案,實際上就是”加緩存避免重復計算”。

三、什么是“動態規划”

(1)比較“記憶化搜索”與“動態規划”

由上面的介紹我們就可以引出動態規划的概念:

  • "記憶化搜索"或者我們稱"重疊子問題"的加緩存優化的實現,我們的思考路徑是"自頂向下"。即為了解決數據規模大的問題,我們“假設”已經解決了數據規模較小的子問題。
  • “動態規划”就是上述"循環版本"的實現,我們思考問題路徑是"自下而上"。實際上,我們是先“真正地”解決了數據規模較小的問題,然后一步一步地解決了數據規模較大的問題。

(2)“動態規划”的官方定義

下面我們給出“動態規划”的官方定義:

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.

將原問題拆解成若干子問題,同時保存子問題的答案,使得每個子問題只求解一次,最終獲得原問題的答案。

(3)針對“動態規划”問題的一般思考路徑

我們通常的做法是:使用記憶化搜索的思路思考原問題,但是使用動態規划的方法來實現。即“從上到下”思考,但是“從下到上”實現。

四、總結

對於一個遞歸結構的問題,如果我們在分析它的過程中,發現了它有很多“重疊子問題”,雖然並不影響結果的正確性,但是我們認為大量的重復計算是不環保,不簡潔,不優雅,不高效的,因此,我們必須將“重疊子問題”進行優化,優化的方法就是“加入緩存”,“加入緩存”的一個學術上的叫法就是“記憶化搜索”。

另外,我們還發現,直接分析遞歸結構,是假設更小的子問題已經解決給出的實現,思考的路徑是“自頂向下”。但有的時候,“自底向上”的思考路徑往往更直接,這就是“動態規划”,我們是真正地解決了更小規模的問題,在處理更大規模的問題的時候,直接使用了更小規模問題的結果。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM