動態規划法(一)從斐波那契數列談起


動態規划法與分治方法

  動態規划(Dynamic Programming)與分治方法相似,都是通過組合子問題的解來求解原問題。不同的是,分治方法通常將問題划分為互不相交子問題遞歸地求解子問題,再講它們的解組合起來,求出原問題的解。而動態規划應用於子問題重疊的情況,即不用的子問題具有公共的子子問題。在這種情況下,如果采用分治算法,則分治算法會做許多不必要的工作,它會反復地求解那些公共子子問題。對於動態規划法,它對每個子子問題只求解一次,將其保存在一個表格中,從而無需每次求解一個子子問題時都重新計算,避免了這種不必要的計算工作。
  也就是說,動態規划法與分治方法相比,是用空間來換時間,而時間上獲得的效益是很客觀的,這是一種典型的時空平衡(time-memory trade-off)的策略。通常,動態規划法用來求解最優化問題(optimization problem),如斐波那契數列求值問題,鋼條切割問題,0-1背包問題,矩陣鏈乘法問題,最長公共子序列(LCS)問題,最優二叉搜索樹問題等。
  一般情況下,動態規划算法的步驟如下:

  1. 刻畫一個最優解的結構特征。
  2. 遞歸地定義最優解的值。
  3. 計算最優解的值,通常采用自底向上的方法。
  4. 利用計算出的信息構造一個最優解。

  接下來,我們將從斐波那契數列求值這個簡單的例子入手,來分析動態規划法的具體步驟和優點。

斐波那契數列

  斐波那契數列記為\(\{f(n)\}\),其表達式如下:

\[\left\{ \begin{array}{lr} f(0)=0\\ f(1)=1\\ f(n)=f(n-1)+f(n-2),n\geq 2 \end{array} \right. \]

  具體寫出前幾項,就是:0,1,1,2,3,5,8,13,21,34,55,89,144,233......
  接下來,我們將會采用遞歸法和動態規划法來求解該數列的第n項,即f(n)的值。

遞歸法求解

  首先,我們采用遞歸法來求解斐波那契數列的第n項\(f(n)\),其算法描述如下:

function fib(n)
    if n = 0 return 0
    if n = 1 return 1
    return fib(n − 1) + fib(n − 2)

分析上述偽代碼,先是定義一個函數fib(n),用來計算斐波那契數列的第n項,當\(n\geq 2\)時,它的返回值會調用函數fib(n-1)和fib(n-2).當\(n=5\)時,計算fib(5)的函數調用情況如下圖所示:

在計算fib(5)時,fib(5)調用1次,fib(4)調用1次,fib(3)調用2次,fib(2)調用3次,fib(1)調用5次,fib(0)調用3次,一共調用函數fib()15次。由此,我們可以看到,在計算fib(5)時,存在多次重復的fib()函數的調用,當n增大時,重復調用的次數會急劇增加,如計算fib(50)時,fib(1)和fib(0)大約會被調用\(2.4\times10^{10}\)次。由此可見,該算法的效率並不是很高,因為該算法的運行時間是指數時間。
  我們用Python實現上述算法,並計算f(38)的值及運算時間。Python代碼如下:

import time

# recursive method
def rec_fib(n):
    if n <= 1:
        return n
    else:
         return rec_fib(n-1) + rec_fib(n-2)
    
# time cost of cursive method
t1 = time.time()
t = rec_fib(38)
t2 = time.time()

print('結果:%s, 運行時間:%s'%(t, t2-t1))

輸出結果如下:

結果:39088169, 運行時間:22.93831205368042

動態規划法求解

  在使用遞歸法來求解斐波那契數列的第n項時,我們看到了遞歸法的不足之處,因為遞歸法在使用過程中存在大量重復的函數調用,因此,效率很差,運行時間為指數時間。為了解決遞歸法存在的問題,我們可以嘗試動態規划法,因為動態規划法會在運行過程中,保存上一個子問題的解,從而避免了重復求解子問題。對於求解斐波那契數列的第n項,我們在使用動態規划法時,需要保存f(n-1)和f(n-2)的值,犧牲一點內存,但是可以顯著地提升運行效率。
  動態規划法來求解斐波那契數列第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

在上述偽代碼中,並沒有存在重復求解問題,只是在每次運行過程中,保存上兩項的值,再利用公式\(f(n)=f(n-1)+f(n-2)\)來求解第n項的值。用Python實現上述過程,代碼如下:

import time

# bottom up approach of Dynamic Programming
def dp_fib(n):
    previousFib = 0
    currentFib = 1
    if n <= 1:
        return n

    # repeat n-1 times
    for _ in range(n-1):
        newFib = previousFib + currentFib
        previousFib = currentFib
        currentFib = newFib

    return currentFib

# time cost of DP method
t1 = time.time()
t = dp_fib(38)
t2 = time.time()

print('結果:%s, 運行時間:%s'%(t, t2-t1))

輸出結果如下:

結果:39088169, 運行時間:0.0

  顯然,使用動態規划法來求解斐波那契數列第n項的運行效率是很高的,因為,該算法的時間復雜度為多項式時間。

參考文獻

  1. 算法導論(第四版)
  2. https://www.cs.upc.edu/~jordicf/Teaching/programming/pdf/IP07_Recursion.pdf
  3. https://www.saylor.org/site/wp-content/uploads/2011/06/Dynamic-Programming.pdf

附錄

用遞歸法和動態規划法來求解該數列的第n項,完整的Python代碼如下:

# calculate nth item of Fibonacci Sequence
import time

# recursive method
def rec_fib(n):
    if n <= 1:
        return n
    else:
         return rec_fib(n-1) + rec_fib(n-2)

# bottom up approach of Dynamic Programming
def dp_fib(n):
    previousFib = 0
    currentFib = 1
    if n <= 1:
        return n

    # repeat n-1 times
    for _ in range(n-1):
        newFib = previousFib + currentFib
        previousFib = currentFib
        currentFib = newFib

    return currentFib

 # time cost of cursive method
t1 = time.time()
t = rec_fib(38)
t2 = time.time()
print('結果:%s, 運行時間:%s'%(t, t2-t1))
# time cose of DP method
s = dp_fib(38)
t3 = time.time()
print('結果:%s, 運行時間:%s'%(t, t3-t2))

輸出結果如下:

結果:39088169, 運行時間:22.42628264427185
結果:39088169, 運行時間:0.0

完整的Java代碼如下:

package DP_example;

import java.util.Date;
import java.math.BigInteger;

public class fib {
    // 主函數
    public static void main(String[] args) {
        Date start_time =  new Date(); //開始時間
        int n = 38;
        BigInteger t1 = DP_fib(n);  // 動態規划法求解
        Date end_time1 =  new Date(); // 結束時間
        Long cost_time1 = end_time1.getTime()-start_time.getTime();  // 計算時間,返回毫秒數
        System.out.println(String.format("The fib(%d) is %s.\nCost time is %.3fs.", n, t1, cost_time1*1.0/1000));


        BigInteger t2 = rec_fib(n);  // 遞歸法求解
        Date end_time2 =  new Date(); // 結束時間
        Long cost_time2 = end_time2.getTime()-end_time1.getTime();  // 計算時間,返回毫秒數
        System.out.println(String.format("The fib(%d) is %s.\nCost time is %.3fs.", n, t2, cost_time2*1.0/1000));

    }

    // 利用遞歸方法計算斐波那契數列的第n項
    public static BigInteger rec_fib(int n){
        if(n == 0)
            return BigInteger.ZERO;
        if(n ==1)
            return BigInteger.ONE;
        else
            return rec_fib(n-1).add(rec_fib(n-2));
    }

    // 利用動態規划法(DP)計算斐波那契數列的第n項
    public static BigInteger DP_fib(int n){
        if(n == 0)
            return BigInteger.ZERO;
        if(n == 1)
            return BigInteger.ONE;
        else {
            BigInteger previousFib = BigInteger.ZERO;
            BigInteger currentFib = BigInteger.ONE;
            BigInteger newFib;

            for(int i=1; i<n; i++){ // 重復循環n-1次
                newFib =  previousFib.add(currentFib);
                previousFib = currentFib;
                currentFib = newFib;
            }

            return currentFib;
        }
    }
}

輸出的結果如下所示:

The fib(38) is 39088169.
Cost time is 0.001s.
The fib(38) is 39088169.
Cost time is 2.029s.

注意:本人現已開通兩個微信公眾號: 用Python做數學(微信號為:python_math)以及輕松學會Python爬蟲(微信號為:easy_web_scrape), 歡迎大家關注哦~~


免責聲明!

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



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