教你徹底學會動態規劃 入門篇

2021-10-03 15:30:19 字數 3920 閱讀 3609

動態規劃相信大家都知道,動態規劃演算法也是新手在剛接觸演算法設計時很苦惱的問題,有時候覺得難以理解,但是真正理解之後,就會覺得動態規劃其實並沒有想象中那麼難。網上也有很多關於講解動態規劃的文章,大多都是敘述概念,講解原理,讓人覺得晦澀難懂,即使一時間看懂了,發現當自己做題的時候又會覺得無所適從。我覺得,理解演算法最重要的還是在於練習,只有通過自己練習,才可以更快地提公升。話不多說,接下來,下面我就通過乙個例子來一步一步講解動態規劃是怎樣使用的,只有知道怎樣使用,才能更好地理解,而不是一味地對概念和原理進行反覆琢磨。

數字三角形(poj1163)

在上面的數字三角形中尋找一條從頂部到底邊的路徑,使得路徑上所經過的數字之和最大。路徑上的每一步都只能往左下或 右下走。只需要求出這個最大和即可,不必給出具體路徑。 三角形的行數大於1小於等於100,數字為 0 - 99

輸入格式:

5 //表示三角形的行數 接下來輸入三角形

73 8

8 1 0

2 7 4 4

4 5 2 6 5

要求輸出最大和

接下來,我們來分析一下解題思路:

首先,肯定得用二維陣列來存放數字三角形

然後我們用d( r, j) 來表示第r行第 j 個數字(r,j從1開始算)

我們用maxsum(r, j)表示從d(r,j)到底邊的各條路徑中,最佳路徑的數字之和。

因此,此題的最終問題就變成了求 maxsum(1,1)

當我們看到這個題目的時候,首先想到的就是可以用簡單的遞迴來解題:

if ( r == n)                

maxsum(r,j) = d(r,j)

else

maxsum( r, j) = max + d(r,j)

根據上面這個簡單的遞迴式,我們就可以很輕鬆地寫出完整的遞迴**:

#include #include #define max 101  

using namespace std;

int d[max][max];

int n;

int maxsum(int i, int j)

int main()

對於如上這段遞迴的**,當我提交時,會超時。。。。

為什麼會超時呢?

答案很簡單,因為我們重複計算了,當我們在進行遞迴時,計算機幫我們計算的過程如下圖:

[外鏈轉存失敗,源站可能有防盜煉機制,建議將儲存下來直接上傳(img-k6m78y7q-1583634626494)(

就拿第三行數字1來說,當我們計算從第2行的數字3開始的maxsum時會計算出從1開始的maxsum,當我們計算從第二行的數字8開始的maxsum的時候又會計算一次從1開始的maxsum,也就是說有重複計算。這樣就浪費了大量的時間。也就是說如果採用遞規的方法,深度遍歷每條路徑,存在大量重複計算。則時間複雜度為 2的n次方,對於 n = 100 行,肯定超時。 

接下來,我們就要考慮如何進行改進,我們自然而然就可以想到如果每算出乙個maxsum(r,j)就儲存起來,下次用到其值的時候直接取用,則可免去重複計算。那麼可以用n方的時間複雜度完成計算。因為三角形的數字總數是 n(n+1)/2

根據這個思路,我們就可以將上面的**進行改進,使之成為記憶遞迴型的動態規劃程式:

#include #include using namespace std;

#define max 101

int d[max][max];

int n;

int maxsum[max][max];

int maxsum(int i, int j)

return maxsum[i][j];

} int main()

cout << maxsum(1,1) << endl;

}

當我們提交如上**時,結果就是一次ac

雖然在短時間內就ac了。但是,我們並不能滿足於這樣的**,因為遞迴總是需要使用大量堆疊上的空間,很容易造成棧溢位,我們現在就要考慮如何把遞迴轉換為遞推,讓我們一步一步來完成這個過程。

我們首先需要計算的是最後一行,因此可以把最後一行直接寫出,如下圖:

[外鏈轉存失敗,源站可能有防盜煉機制,建議將儲存下來直接上傳(img-ia2tvy55-1583634626494)(

現在開始分析倒數第二行的每乙個數,現分析數字2,2可以和最後一行4相加,也可以和最後一行的5相加,但是很顯然和5相加要更大一點,結果為7,我們此時就可以將7儲存起來,然後分析數字7,7可以和最後一行的5相加,也可以和最後一行的2相加,很顯然和5相加更大,結果為12,因此我們將12儲存起來。以此類推。。我們可以得到下面這張圖:

[外鏈轉存失敗,源站可能有防盜煉機制,建議將儲存下來直接上傳(img-katwogh6-1583634626495)(

然後按同樣的道理分析倒數第三行和倒數第四行,最後分析第一行,我們可以依次得到如下結果

[外鏈轉存失敗,源站可能有防盜煉機制,建議將儲存下來直接上傳(img-ouxw9gnx-1583634626495)(

上面的推導過程相信大家不難理解,理解之後我們就可以寫出如下的遞推型動態規劃程式

#include #include using namespace std; 

#define max 101

int d[max][max];

int n;

int maxsum[max][max];

int main()

我們的**僅僅是這樣就夠了嗎?當然不是,我們仍然可以繼續優化,而這個優化當然是對於空間進行優化,其實完全沒必要用二維maxsum陣列儲存每乙個maxsum(r,j),只要從底層一行行向上遞推,那麼只要一維陣列maxsum[100]即可,即只要儲存一行的maxsum值就可以。

對於空間優化後的具體遞推過程如下:
[外鏈轉存失敗,源站可能有防盜煉機制,建議將儲存下來直接上傳(img-ikc3jgrw-1583634626496)(

[外鏈轉存失敗,源站可能有防盜煉機制,建議將儲存下來直接上傳(img-6likasnd-1583634626497)(

接下裡的步驟就按上圖的過程一步一步推導就可以了。進一步考慮,我們甚至可以連maxsum陣列都可以不要,直接用d的第n行直接替代maxsum即可。但是這裡需要強調的是:雖然節省空間,但是時間複雜度還是不變的。

依照上面的方式,我們可以寫出如下**:

#include #include using namespace std; 

#define max 101

int d[max][max];

int n;

int * maxsum;

int main()

**遞迴到動規的一般轉化方法:**

遞迴函式有n個引數,就定義乙個n維的陣列,陣列的下標是遞迴函式引數的取值範圍,陣列元素的值是遞迴函式的返回值,這樣就可以從邊界值開始, 逐步填充陣列,相當於計算遞迴函式值的逆過程。

**動規解題的一般思路:**

2.確定狀態

3.確定一些初始狀態(邊界狀態)的值

4.確定狀態轉移方程

[外鏈轉存失敗,源站可能有防盜煉機制,建議將儲存下來直接上傳(img-dv2efhco-1583634626497)(

能用動規解決的問題的特點

1) 問題具有最優子結構性質。如果問題的最優解所包含的 子問題的解也是最優的,我們就稱該問題具有最優子結 構性質。

2) 無後效性。當前的若干個狀態值一旦確定,則此後過程的演變就只和這若干個狀態的值有關,和之前是採取哪種手段或經過哪條路徑演變到當前的這若干個狀態,沒有關係。

教你徹底學會動態規劃 高階篇

話不多說,來看如下例題,也是在動態規劃裡面遇到過的最頻繁的乙個題,本題依然來自於北大poj 最長公共子串行 poj1458 給出兩個字串,求出這樣的乙個最長的公共子串行的長度 子串行中的每個字元都能在兩個原串中找到,而且每個字元的先後順序和原串中的先後順序一致。sample input abcfbc...

動態規劃入門篇

動態規劃相信大家都知道,動態規劃演算法也是新手在剛接觸演算法設計時很苦惱的問題,有時候覺得難以理解,但是真正理解之後,就會覺得動態規劃其實並沒有想象中那麼難。在上面的數字三角形中尋找一條從頂部到底邊的路徑,使得路徑上所經過的數字之和最大。路徑上的每一步都只能往左下或 右下走。只需要求出這個最大和即可...

狀態壓縮動態規劃入門篇

狀態壓縮動態規劃 動態規劃的狀態有時候比較難,不容易表示出來,需要用一些編碼技術,把狀態壓縮的用簡單的方式表示出來。典型方式 當需要表示乙個集合有哪些元素時,往往利用2進製用乙個整數表示。一般有個資料 n 16 或者 n 32 這個很可能就是狀態dp的標誌,因為我們要用乙個int的二進位制來表示這些...