1. 程式人生 > >動態規劃系列(1)——動態規劃入門

動態規劃系列(1)——動態規劃入門

一般的,我們常用的解決問題的方法有暴力解決法、分而治之、二分法、貪心法和動態規劃法。在你遇到一個問題怎麼想都想不出其解法的時候,很可能就需要用到動態規劃了;在你的題目中出現最優、最多、最好等字眼的時候,很可能可以使用動態規劃問題來解決了。

那麼什麼是動態規劃(Dynamic Programming)呢?動態規劃和分治思想、遞迴有著千絲萬縷的關係。簡單來說,分治思想是把一個問題分成一個一個的互不相關小問題,小問題再細分直至不可分(類似於把一根木棍切啊切);遞迴就是在程式執行的過程中呼叫自身的一種程式設計技巧;動態規劃通過尋找過程狀態轉移方程,將一個問題分解為子問題求解,但是子問題之間可能會有重複,因此如果單純的使用遞迴方法來實現動態規劃問題時間複雜度會比較高。不過動態規劃問題的本質就是遞迴,這是因為我們在分析動態規劃問題的過程中,需要狀態轉移方程,這個狀態轉移方程本質上就是遞迴。後面實現的過程中是否使用遞迴只是實現的不同而已,其本質就是遞迴。

動態規劃有三個最基本的元素:最優子結構、狀態轉移方程和邊界。狀態轉移方程用於描述將當前狀態的解分解為更小狀態的關係式;邊界即狀態轉移方程的截止條件;最優子結構即確保通過狀態轉移方程所選擇的子問題也能給出最優的解。

以最最基礎的fibnacci問題為例:其基礎的遞推關係式為:

fib(i)=max\left\{\begin{matrix} 1 & i<3\\ fib(i-1)+fib(i-2) & otherwise \end{matrix}\right.

如果現在需要計算fib(5),按照規則需要計算fib(4) + fib(3),然後分別計算fib(4)和fib(3),具體的計算過程如下圖所示:

這一個遞迴的,層層向下的呼叫過程就是動態規劃解fibnacci問題的過程,一般的,動態規劃問題的展開圖就像上面這樣呈樹狀結構。而這樣的結構示意圖只是我們分析動態規劃問題的第一步(通過寫出的狀態轉移方程對問題進行結構上的分析)。接下來通過程式設計解決這樣一個問題的時候有兩種選擇:一種是自頂向下,也就是通過遞迴的方式來解決;一種是自下向上,通過記錄每一步的狀態量來減少時間複雜度(用空間換時間)。下面分別對這兩種方法進行講解:

方法一:遞迴

採用遞迴實質上就是按照上圖中的二叉樹的先序遍歷的順序執行,其中每有一個節點就代表執行一次,因此其時間複雜度是O(2^n),這種遞迴解法太暴力了,時間複雜度太高。在LeetCode上上傳這樣的程式碼即使結果正確也不能Accept。

int fib(int idx)
{
    if(idx < 1)
    {
        return 0;
    }
    
    if(idx < 3)
    {
        return 1;
    }
    
    return fib(idx - 1) + fib(idx - 2);

}

方法二:新增備忘錄的遞迴

按照二叉樹的遞迴方法會存在很多重複的計算,如何避免這些重複的計算呢?我們提前申請一個數組,將已經計算過的fib(i)的值存在arr[i]中,這樣的做法形象的稱為備忘錄。在後面呼叫fib(i)函式時,若arr[i] != -1則代表這個數已經計算過了,直接取其值就好了。如此一來每個數的fib函式只需要計算一次,因此其時間複雜度和空間複雜度均為O(n)。

// global array
int arr[10];

int fib(int idx)
{
    if(idx < 1)
    {
        return 0;
    }
    
    if(idx < 3)
    {
        return 1;
    }
    
    if(arr[idx] != 0)
    {
        return arr[idx];
    }
    else
    {
        arr[idx] = fib(idx - 1) + fib(idx - 2);
        return arr[idx];
    }

}

方法三:自底向上的迭代法

採用遞迴的方法始終會有較大的空間消耗,而採用自底向上的迭代方法可以將空間複雜度降低到O(1)級別。 

int fib(int idx)
{
    if(idx < 1)
    {
        return 0;
    }
    
    if(idx < 3)
    {
        return 1;
    }
    
    int temp_a = 1;
    int temp_b = 1;
    int temp_c;
    
    for(int i = 3; i < idx; i++)
    {
        temp_a = temp_b;
        temp_c = temp_a + temp_b;
        temp_b = temp_c;
    }
    
    return temp_c;
}

 這只是最簡單的動態規劃為題,舉這個例子只是為了直觀的介紹動態規劃的概念,在後面會介紹一些常見的動態規劃問題的解決思路和實現程式碼,並且難度會有很大程度的提升,從一維問題升級到二級問題。