動態規劃(dynamic programming),簡稱 dp。是刷leetcode、刷ob的主要算法之一。(逃c++
動態規劃其實有挺多問題有他使用的場景的,好比是數據庫的JOIN 。若是你深刻了解過數據庫原理的話,數據庫的多表 JOIN 是真的複雜。算法
動態規劃,和遞歸是相似的,都是一種分而治之的思想。遞歸的思想,就一些狀況下實際上是存在着重複計算的。而遞歸可優化成動態規劃的緣由,或者說動態規劃的性質吧,就是遞歸中的每一步都是最優解,每一步的最優解均可以根據上一步的結果得出。而且每一步中都存在着重複計算的問題。數據庫
這樣說有點抽象,具個例子吧,就是幾乎每本教材將遞歸的時候都會講到的斐波那契數列(兔子隊列)。本來的問題描述是這樣的,假設第1個月有1對剛誕生的兔子,第2個月進入成熟期,第3個月開始生育兔子,而1對成熟的兔子每個月會生1對兔子,兔子永不死去……那麼,由1對初生兔子開始,12個月後會有多少對兔子呢?編程
這個問題。答案確定是 當前月的兔子書 = 上個月的兔子數 + 新生的兔子數。數組
而新生的兔子數又恰好又等於 上上個月的兔子數。 因此能夠得出結論是 當前月的兔子書 = 上個月的兔子數 + 上上個月的兔子數。bash
抽象出來就是 網絡
用 c++ 描述就是數據結構
long fib(int n) {
return n < 2 ? 1 : fib(n - 1) + fib(n - 2);
}
複製代碼
數學描述和程序實現是簡潔的,可是這程序就存在這不少的重複計算編程語言
好比:計算 F(4)函數
F(4) = F(3) + F(2);
F(3) = F(2) + F(1); //重複計算了 F(2)
F(2) = F(1) + F(0); //重複計算了 F(1)
F(1) = 1;
//...
複製代碼
就意味着,程會序算得很慢,你能夠試試 fib(100)看看。速度感人了。這裏的時間複雜度但是O(2^n)
但這也存在這很是明顯的優化空間。
首先,F(10) 這樣的函數是不會由於外部狀態發生改變的,也就是說跟什麼時間、網絡io是一點關係都沒有的。F(10) 的答案是恆定的,F(9) 、F(8)的答案也是是恆定的。符合每一步都是最優解,固然也符合每一步均可以經過上一步的結果中得出答案。
其次,這是每一步中都會重複計算的問題。
那麼怎樣優化。
若是沒有學過動態規劃,正常人最快想到的方法應該是哈希表吧。既然是重複計算,我將重複的結果保存一下就行了。也很容易寫出這樣的代碼
long fib(int n, map<int, long> &map) {
if (map.count(n) > 0)
return map[n];
else {
long val = n < 2 ? 1 : fib(n - 1, map) + fib(n - 2, map);
map.insert({n, val});
return val;
}
}
複製代碼
雖然說這種方式是可行了。好比計算 Fib(100),比上面的一段代碼快多了。 但這種方式不夠優雅。本質上也只是在遞歸的層面上作了一些優化。而哈希表 這種數據結構存儲值的話,內存會有一些浪費,並且要處理key衝突的問題。
想一想計算 fib(100),也就是100次運算,用個數組存儲值就能夠了。因此能夠寫成這樣。(這其實已是dp的思路了)
long fib(int n, vector<long> &array) {
if (array[n] != 0)
return array[n];
else {
long val = n < 2 ? 1 : fib(n - 1, array) + fib(n - 2, array);
array[n] = val;
return val;
}
}
複製代碼
遞歸的思惟是自頂向下,和咱們在解數學題的時候的思惟是一致的。 而若是用自底向上的思路呢?就會寫成這樣。
long fib(int n) {
if (n < 2)
return 1;
vector<long> array(n + 1, 0);
array[0] = 1;
array[1] = 1;
for (int i = 2; i <= n; i++) {
array[i] = array[i - 1] + array[i - 2];
}
return array[n];
}
複製代碼
應該來說就更合符,過程式編程語言或者說是機器語言的思惟了。我會以爲這種方式比較難想一點。
若是用自底向上的思惟。從上面的代碼能夠看出,其實真的不須要一個數組的空間。只需知道上一步的值和上上一步的值就能夠了。
就能夠寫成這樣
long fib(int n) {
if (n < 2)
return 1;
long p1 = 1;
long p2 = 1;
for (int i = 2; i <= n; i++) {
long val = p1 + p2;
p1 = p2;
p2 = val;
}
return p2;
}
複製代碼
再精簡一下,去掉 if 就變成這樣了
long fib(int n) {
long p1 = 0;
long p2 = 1;
for (int i = 1; i <= n; i++) {
long val = p1 + p2;
p1 = p2;
p2 = val;
}
return p2;
}
複製代碼
總結一下就是,若是純粹用遞歸的思惟去解決問題,實際上是簡單的,是符合咱們之前解決數學問題的思惟的。這種思惟在編程語言的實現中,就有重複計算的問題。動態規劃會用表格化的思想去解決問題。
多是我思惟定型了吧,就算大學不怎麼學習數學,也學了十年數學。(也沒怎麼acm訓練)之前的思惟很難轉向計算機那種思惟,我解決問題的方式每每是先寫遞歸的實現,再慢慢地轉成自底向上的動態規劃。。。