1. 程式人生 > >動態規劃學習筆記 初識動態規劃

動態規劃學習筆記 初識動態規劃

寫在前面

注意:此文章僅供參考,如發現有誤請及時告知。

更新日期:2018/3/16,2018/12/03


動態規劃介紹

動態規劃,簡稱DP(Dynamic Programming)
簡介1 簡介2

動態規劃十分奇妙,它可以變身為記憶化搜尋,變身為遞推,甚至有時可以簡化成一個小小的算式。

動態規劃十分靈活,例如 NOIP2018 PJ T3 擺渡車 ,寫法有很多很多,但時間、記憶體卻各有差異。

動態規劃十分簡單,有時候一個小小的轉移方程就能解決問題。

動態規劃十分深奧,有時你會死也想不出合適的轉移方程,有時你會被後效性困擾,有時動態規劃的同時還有許多蜜汁優化。

動態規劃在NOIP中十分重要,我目前為止參加的\(NOIP_{2017 PJ} \& NOIP_{2018PJ}\)

都有一道動態規劃,而且都是\(T3\)。(估計普及考綱比較窄,要出難題只有DP了)


問題引入

還是這道題...... 數塔問題!!!

img

這裡我們選擇動態規劃來解決.
我們不難理解,對於每一個元素,它到頂層的最大值是確定的,也就是說,從頂層到任何一個元素的最大值都是確定的.比如,對於第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