序言
書中介紹動態規劃比較複雜,看得不是特別地懂,我將從我自己理解的動態規劃來做一些記錄和介紹。
什麼是動態規劃
在說動態規劃之前,我們先談一談斐波那契數列。斐波那契數列是第n個元素和第n-1個和第n-2個元素之和即 f(n) = f(n-1)+f(n-2)(這個方程在DP裏面也稱爲狀態轉移方程)。基於這個性質,我們很容易想到可以構建一棵遞歸二叉樹:
假如我們使用遞歸的算法的話,我們將會得到一個指數級的時間,這是一個非常恐怖的事情,用代碼是實現是這樣的:
int fib( int n )
{
if( n=0)
return 0;
if( n==1)
return 1;
return fib(n-1) + fib(n-2);
}
爲什麼會這樣呢。其實我們可以發現,在這個樹中,其實重複運算了很大一部分:
像這樣,隨着元素的增加,重複運算的次數,將會越來越大,就會造成一種指數級別的增長。我們就要思考如何去優化一下這種算法呢。其實問題是出在重複運算上面的。假如我們只讓它計算一次的話,我們就可以將O(2n)降到O(n),這樣我們就有了叫記憶化搜索的方法,就是設置一個標誌量,這樣就會減少重複的遞歸,這種方式也叫做是自上而下的解決辦法:
arr = vector<int>(n+1, -1)
int fib( int n )
{
if( n=0)
return 0;
if( n==1)
return 1;
if (memo[n] == -1) //設置了一個標誌量
return fib(n-1) + fib(n-2);
return memo[n];
}
而當然也有一種自下到上的解決方法,用來解決斐波那契數列數列的話可以這樣寫:
int fib(int n)
{
vector<int> memo(n+1, -1);
memo[0] = 0;
memo[1] = 1;
for (int i=2; i<=n; i++)
memo[i] = memo[i-1] + memo[i-2];
return memo[n];
}
所以可以解答什麼是動態規劃的問題了:
動態規劃:將原問題拆解決成若干個子問題,同時保存子問題的答案,是每個子問題都只求解一次,最終獲得原問題的答案
且動態規劃通常用來求解決最優解的問題。根據算法導論,我們可以有四個步驟來設計一個動態規劃算法:
- 刻畫一個最優解的結構特徵
- 遞歸地定義最優解的值
- 計算最優解的值,通常採用自底向上的方法
- 利用計算的信息構造一個最優解
動態規劃的兩種實現方法
對於上面提到的自頂向下和自下而上的解決方法就是動態規劃的兩種實現方法。
對於自頂向下的求解方法,我們稱爲帶備忘的自頂向下法,這種方法還是按照遞歸的方式來寫的,只是會用一個數組或者哈希表來保存每個子問題的解。當遇到相同的子問題的時候不會再進行重複求解,這個過程稱之爲帶備忘的
而自下而上的方法叫做自底向上法,這個方法的話,相對來說要難一點,簡單的說,就是需要找到一個恰當定義子問題的概念,使得任何子問題的求解都是依賴於更小問題的求解。
其實這兩種方法得到的算法具有相近的漸近時間。
動態規劃的一般解題方法
在力扣上面刷了幾道題後發現,有些題跟斐波那契數列的優化過程是很像的,大多可以通過暴力遞歸(這種往往會付出較大的時間空間代價)→帶備忘錄的遞歸解法 → 非遞歸的動態規劃解法這樣的過程。當然這是我們思考的方向,雖然有這種流程,當時並不代表就能夠解決動態規劃的問題。
根據《算法導論》裏面說的動態規劃的原理,解決動態規劃問題我們就需要描述,解決最優子結構問題和重疊問題。解決最優子結構的問題,我看的不是特別的懂,這裏不做過多的闡述。但是重疊問題(遞歸算法會重複的求解同一個子問題)的解決,是通過一個記憶化功能或者說是備忘錄,這種方式的實現就是用一個數組或者哈希表來存有標誌量作爲備忘錄,就像是上面的斐波那契數列的設置以數組全部設置爲-1,遇到了相同的子問題不會重複的求解而是直接的返回。
然後,解決動態規劃問題的大致的步驟分爲三步:
- 建立狀態轉移方程
- 緩存並複用以往結果
- 按順序從小往大算
在我遇到的題中,參考人家的解法,大多就是分成這幾種步驟來分別完成的,但是其實並不簡單。動態規劃問題是一個大而難的問題,相當的靈活,不能夠通過簡單的幾句話就能夠理解得很透徹,要形成解決動態規劃的思考方式,可以通過做一些題,總結其方法,尋找裏面的規律。