1. 程式人生 > >遞迴、分治和動態規劃的關係

遞迴、分治和動態規劃的關係

內容會持續更新,有錯誤的地方歡迎指正,謝謝!

動態規劃

如果大問題分解為很多小問題後,小問題有互相重疊部分,則用遞迴的思路來分析問題,再用陣列儲存中間結果+迴圈的思路來寫程式碼!

動態規劃的三個特徵

  1. 適用於最優解問題
  2. 有大量的重複子問題
  3. 子問題之間有依賴(不獨立)

與遞迴的關係:這些重複的子問題,DP演算法將其結果用一維或二維陣列(鄰接矩陣)儲存下來,等下一次又要計算該子問題時,直接用已計算好的;而遞迴卻不是這樣,它會一遍又一遍地計算這些重複的子問題,從而效率狂降。子問題重複率較高的遞迴演算法可改寫成動態規劃,但不是所有遞迴演算法都適合改成動態規劃。

與分治的關係:在分治法中,有大量的重複子問題,且它們之間無依賴。

如何寫動態規劃題目的程式碼

我覺得動態規劃題目是很簡單的,因為只要推出了遞推式,什麼0-1揹包問題作業排程問題最長共同子序列LCS問題等等,程式碼根據遞推式便可一氣呵成。

那又如何寫分治題目的程式碼

舉例:
這裡寫圖片描述

這裡寫圖片描述

動態規劃的兩種型別

1.自頂向下的動態規劃實現:用的遞迴。
2.自底向上的動態規劃實現:用的迭代。

斐波拉契數列

1.普通的遞迴實現的動態規劃:效率特別低,有大量的重複計算,指數級的時間複雜度

int Fibo(int n)
{
    if(n==0)
        return 0;
    if(n==1)
        return
1; return Fibo(n-1)+Fibo(n-2); }

2.自底向上的動態規劃實現:會記錄重複子問題結果的改進版迭代。只要有儲存已經計算出的值的空間,就能把這項技術應用到任何遞迴計算中,就能把演算法從指數級執行時間向線性時間改進。

int Fibo(int n)
{
    int temp[n];
    temp[0]=0;
    temp[1]=1;
    for(int i=2;i<n+1;++i)
    {
        temp[i]=temp[i-1]+temp[i-2];
    }
    return temp[n];
}

3.自頂向下的動態規劃實現:

會記錄重複子問題結果的改進版遞迴。儲存它所計算的每一個值(正如下方程式碼最末的步驟),並通過檢查所儲存的值,來避免重新計算它們的任何項(正如最初的步驟)。

要先寫出遞推式,遞推式:
n=0時,f(n)=0,即在f(n)函式中return 0
n=1時,f(n)=1,即在f(n)函式中return 1
n>1時,f(n)=f(n-1)+f(n-2),即在f(n)函式中return f(n-1)+f(n-2)
再用array陣列來記錄計算出的結果,避免重複計算一些值。

#include <iostream>
#include <string.h>
using namespace std;
#define N 12
int array[N] = {0};
int Fibo(int n)
{
    //不等於初始值0,則表示該元素已經求解過了,直接用其值即可。
    if(array[n]!=0)
        return array[n];

    //完成按著遞推式來寫邏輯,即可!
    if(n==0)
        return array[n] = 0;
    if(n==1)
        return array[n] = 1;
    if(n>1)
        return array[n] = Fibo(n-1)+Fibo(n-2);
}
int main()
{
    memset(array,0,sizeof(array));
    cout << Fibo(N) << endl;
    return 0;
}

總結:

為了避免遞迴產生的重複計算,多采用從下而上的迭代實現。所以,一般用自頂向下的遞迴思路來分析問題,並用自底向上的迭代思路來實現問題。自底向上也就是像求解斐波拉契數列那樣,先給出f(0)和f(1)這些已知數值,再在迴圈裡自底向上求解f(2)~f(n)的值,最後返回f(n)即可。

當然,用自頂向下的實現方式,並用陣列記錄計算過的值,一樣可以避免重複的計算,也行~