動態規劃學習筆記 初識動態規劃
寫在前面
注意:此文章僅供參考,如發現有誤請及時告知。
更新日期:2018/3/16,2018/12/03
動態規劃介紹
動態規劃,簡稱DP(Dynamic Programming)
簡介1 簡介2
動態規劃十分奇妙,它可以變身為記憶化搜尋,變身為遞推,甚至有時可以簡化成一個小小的算式。
動態規劃十分靈活,例如 NOIP2018 PJ T3 擺渡車 ,寫法有很多很多,但時間、記憶體卻各有差異。
動態規劃十分簡單,有時候一個小小的轉移方程就能解決問題。
動態規劃十分深奧,有時你會死也想不出合適的轉移方程,有時你會被後效性困擾,有時動態規劃的同時還有許多蜜汁優化。
動態規劃在NOIP中十分重要,我目前為止參加的\(NOIP_{2017 PJ} \& NOIP_{2018PJ}\)
問題引入
還是這道題...... 數塔問題!!!
這裡我們選擇動態規劃來解決.
我們不難理解,對於每一個元素,它到頂層的最大值是確定的,也就是說,從頂層到任何一個元素的最大值都是確定的.比如,對於第3層的第2個元素6,頂層到它的最大值只有一個(9 + 15 + 6 = 30)(但不代表路徑只有一條),不會改變.
所以,我們用一個數組dp來儲存從元素(i, j)到底層的最大值.
#define MAXN 100
int dp[MAXN + 5][MAXN + 5];
仔細觀察分析,不難發現,對於每一個元素dp[i][j]
dp[i][j] = max( dp[i + 1][j], dp[i + 1][j + 1] ) + a[i][j];
即每一個元素到(1, 1)的最大值都是上一層與它相連的兩個元素中較大的一個,再加上這個元素本身的值. 最後的答案即為dp[1][1]
.
不過,我們自頂向下分析,但是卻要自底向上實現,即從最頂層開始分析,寫程式碼時卻要注意for語句要倒過來寫:
for ( int i = N; i >= 1; --i ) for ( int j = 1; j <= i; ++j ) dp[i][j] = max( dp[i + 1][j], dp[i + 1][j + 1] ) + a[i][j];
為什麼會這樣呢?其實不難分析,在算dp[i][j]時,你必須確保dp[i + 1][j]
和 dp[i + 1][j + 1]
已經完成,如果沒有完成,dp[i + 1][j]
和 dp[i + 1][j + 1]
的值就是錯誤的,算出的dp[i][j]
也是錯誤的,這樣結果就不對了。而反過來做,你就會發現i從大的開始,在做dp[i][j]
的時候dp[i + 1][1 ~ N]
都已經做過了。還有,要注意,動態規劃的初始化很重要,有時初始化就會決定你結果對不對。這裡的初始化很簡單,現在給出兩種方法:
memset( dp[N + 1], 0, sizeof( dp[N + 1] ) );//即把dp[N + 1][0...]全部初始化為0.
for ( int i = 1; i <= N; ++i )
dp[i] = a[i];
//下面這個與上面等價:
copy( a[N] + 1, a[N] + N + 1, dp[N] );// copy( 開始地址, 結束地址, 放到的陣列 ); copy( a, a + n, b );即為把a陣列下標為0~n按次序複製到b陣列.
//當然,這樣寫,實現時要注意少一層迴圈:(下面這個是修改後的)
for ( int i = N - 1; i >= 1; --i )
for ( int j = 1; j <= i; ++j )
dp[i][j] = max( dp[i + 1][j], dp[i + 1][j + 1] ) + a[i][j];
//至於為什麼這樣,這裡不再贅述,請自己思考.
這裡再完整地放一放程式碼,實在不會寫的可以參考.
#include<bits/stdc++.h>
using namespace std;
#define MAXN 100
int C, N;
int a[MAXN + 5][MAXN + 5];
int dp[MAXN + 5][MAXN + 5];
void solve(){
scanf( "%d", &N );
memset( dp, 0, sizeof dp );
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= i; ++j )
scanf( "%d", &a[i][j] );
for ( int i = N; i >= 1; --i )
for ( int j = 1; j <= i; ++j )
dp[i][j] = max( dp[i + 1][j], dp[i + 1][j + 1] ) + a[i][j];
printf( "%d\n", dp[1][1] );
}
int main(){
scanf( "%d", &C );
while( C-- ) solve();
return 0;
}
事實上,可以做一個優化:去掉dp陣列,直接用a陣列來做:(節約空間,人人有責)
#include<bits/stdc++.h>
using namespace std;
#define MAXN 100
int C, N;
int a[MAXN + 5][MAXN + 5];
void solve(){
scanf( "%d", &N );
for ( int i = 1; i <= N; ++i )
for ( int j = 1; j <= i; ++j )
scanf( "%d", &a[i][j] );
for ( int i = N - 1; i >= 1; --i )
for ( int j = 1; j <= i; ++j )
a[i][j] += max( a[i + 1][j], a[i + 1][j + 1] );
printf( "%d\n", a[1][1] );
}
int main(){
scanf( "%d", &C );
while( C-- ) solve();
return 0;
}
至於為什麼,請諸位自己理解(很好理解的,選個小一點的資料自己算一算就知道了)。
總結
怎麼樣,找到些感覺了吧?現在我們來學習怎麼寫動態規劃的程式.
第一步,我們要觀察題目是否可以用動態規劃實現。怎麼判斷呢?我們要看它是否可以分成幾個階段,如上題,可以分成1~N層共N個階段,每個階段還可以分成1~i
個元素共i個小階段。然後,我們要看看每個階段的答案是不是確定的,上題中,每一個元素到底層的最大值就是確定的。再看看每個階段是不是有關聯,如果有,還要確定有什麼關聯,是否對於每一個階段都滿足。
第二步,就是確定關聯啦。怎麼確定呢?我們要仔細分析題目,觀察每兩個階段之間的關係。動態規劃的重點也就在這裡,關聯確定了,動態規劃基本上就可以寫下來了。
第三步,確定邊界條件,比如,上題就要把dp[N+1][...]
全部賦值為0,否則就會出錯。
除此之外,還要確定完成的順序,要做某個階段,它需要用到的階段必須先做完。
當然,有時還要新增滾動陣列、優化等。
這樣,一個動態規劃程式就完成啦。
尾聲
當然,動態規劃還有許多分支(揹包DP、區間DP等),以上講的都是最表皮的。那些難一點的,都只好下次再講吧。
最好拿點題目來練一下:洛谷的DP