1. 程式人生 > >DP(動態規劃)個人學習-初步

DP(動態規劃)個人學習-初步

動態規劃是一種重要演算法,其思想,在我個人理解來看,就是把問題逐步地進行簡化,從一個看似複雜的問題,將問題化小,簡化到可以用某條公式(狀態轉移方程)逐步求出每種狀態的解,直到最後求出所要的答案。

比如01揹包問題,揹包容量有限,要儘可能裝入價值大的物品,我們可以假設一個dp陣列,其中dp[j]表示在容量為j的情況下最多能裝入多少價值的物品,這樣就把問題簡化到求一個個狀態了,但是明顯還是不夠用來解決問題,那麼怎麼辦呢?想想:對於每一種物品,都有裝入和不裝入兩種選擇,於是多了這麼一中約束條件:裝\不裝。又由於每種物品只有一件,所以應該從後往前更新,保證每種物品只被裝入一遍,這樣一步步推下來,就得到了我們所要的狀態轉移方程:

dp[j] = max{ dp[j-w[i]]+v[i], dp[i+1] }

具體程式碼則要寫成這樣:

for(int i = 0; i < n ;i++)
    for(int j = W; j >= w[i]; j--)
        dp[j] = max(dp[j], dp[j-w[i]]+v[i]);
好像有點難以理解?其實就是一種數學思想,做動態規劃要對狀態的轉移有足夠清晰的思路,最後還得回到
做題上。下面是HDU上一個關於DP的題目組,要掌握好各種演算法最最最主要的還是多做題!

下面先上一道最簡單的DP題:HDU-1003  Max Sum

題目意思就是給一串數字,要你求出其中和最大的子序列,輸出最大的和,子序列的起始和結束點。

如何做這道題呢?首先簡化下問題,我們可以求到每個數字時以這個數字為結尾的最大子序列和是多少,於是dp陣列出來了,接下來,如何實現狀態轉移?考慮這樣一件事實:對於每一個a[i],dp[i-1]都有加或不加的權利,如果加上他能使其接近答案,那麼就加上。怎樣才算接近答案呢?如果該dp[i-1]大於0,那麼無疑可以加上,不管加上後是正是負在以後的狀態轉移中再來篩除。如果dp[i-1]小於0,那麼這一段可以捨棄掉了,直接從當前數字起始,即dp[i] = a[i]。方程出來了!

上面說的有點繁瑣,主要為了初學者和自己便於理解,具體見程式碼,不正確的地方還請大牛指正!

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;
int a[100000+20], dp[100000+20];

int main()
{
    int T, n, j = 1;
    scanf("%d", &T);
    while(T--) {
        int left = 0, right = 0, maxn;
        memset(dp, 0, sizeof(dp));
        scanf("%d", &n);
        for(int i = 0; i < n; i++)
            scanf("%d", &a[i]);
        maxn = dp[0] = a[0];
        // 根據狀態轉移方程寫出dp陣列,同時記錄最大值,dp[i]最大時即在此時應該記錄的右值為i
        for(int i  = 1; i < n; i++) {
            dp[i] = dp[i-1] > 0 ? dp[i-1]+a[i] : a[i];
            if(maxn < dp[i]) {
                maxn = dp[i];
                right = i;
            }
        }
        left = right;  // 這一句非常重要,漏了這句WA了十幾次,因為要考慮到子序列是一個數的結果,這個初始化不能少
        for(int i = left-1; i >= 0; i--) {  // 從右值開始向前推知道找到這個子序列的左值
            if(dp[i] >= 0)
                left = i;
            else
                break;
        }
        printf("Case %d:\n%d %d %d\n", j++, maxn, left+1, right+1);
        if(T)
            printf("\n");
    return 0;
    }
}
接下來是1003的升級版:HDU-1024  Max Sum Plus Plus

題目意思與上面差不多,改變的是這次要求多段子序列和最大。

這次加了個條件,求n段的子序列的最大和。如何入手呢?首先先寫一下二維的dp方程:

dp[i][j] = max{dp[i][j-1], dp[i-1][k]} + s[i]


dp[i][j]表示前j個數字的i個子序列最大和是多少,很好,狀態轉移方程不難寫出,接下來考慮一下如何優化?二維的寫法我也不是很會寫,能化成一維就更好了。思考一下:在我們的方程中,用到了什麼?其實也只有當前狀態(dp[i][j-1])和上一個狀態(dp[i-1][k])而已!於是可以用兩個一維陣列表示。

#include <iostream>
#include <cstdio>
#include <cstring>

typedef long long ll;
using namespace std;
int s[1000000+20];
ll cur[1000000+20], pre[1000000+20];

int main()
{
    int m, n, j;
    ll maxsum;
    while(~scanf("%d%d", &m, &n)) {
        for(int i = 1; i <= n; i++) {
            scanf("%d", &s[i]);
        }
        memset(cur, 0, sizeof(cur));  // 當前狀態
        memset(pre, 0, sizeof(pre));  // 上一次狀態
        for(int i = 1; i <= m; i++) {  // 從第一個子段開始
            maxsum = -1e9;  // 每次最大值復位
            for(j = i; j <= n; j++) {  // i子段狀態上推下下一個子段
                cur[j] = max(cur[j-1], pre[j-1]) + s[j];  //方程
                pre[j-1] = maxsum;  // 記錄上一個狀態
                maxsum = max(maxsum, cur[j]);  // 最大值
            }
            pre[j-1] = maxsum;
        }
        printf("%lld\n", maxsum);
    }
    return 0;
}

未完待更。。。