1. 程式人生 > >動態規劃---01揹包與記憶化搜尋

動態規劃---01揹包與記憶化搜尋

        動態規劃是一種高效的演算法。在數學和電腦科學中,是一種將複雜問題的分成多個簡單的小問題思想 ----  分而治之。因此我們使用動態規劃的時候,原問題必須是重疊的子問題。運用動態規劃設計的演算法比一般樸素演算法高效很多,因為動態規劃不會重複計算已經計算過的子問題。因為動態規劃又可以稱為“記憶化搜尋”。

        01揹包是介紹動態規劃最經典的例子,同時也是最簡單的一個。我們先看看01揹包的是什麼?

問題(01揹包):
        有n個重量和價值分別為Wi和Vi的物品。從這些物品中挑出總重量不超過W的物品,求所有挑選方案中價值總和的最大值。

       這就是被稱為01揹包的問題。在沒學習動態規劃之前,我們看到這個問題第一反應會用dfs搜尋一遍。那我們先使用這種方法來求解01揹包問題:

//n,W 如題意所述
//這裡的MAXN表示W,n中的最大值(個人習慣)
//如果不習慣這種偷懶的方式
//可以使用MAXW和MAXN分別表示W和n的最大值
int W, n;
//w[i]和v[i]分別表示Wi,Vi
int w[MAXN], v[MAXN];
//從第i個物品開始挑選總重量小於j的部分
int dfs(int i, int j){
    int res;
    //已經沒有剩餘物品
    if(i == n) res = 0;
    //無法挑選第i個物品
    else if(j < w[i]) res = dfs(i+1, j);
    //比較挑和不挑的情況,選取最大的情況
    else res = max(dfs(i+1, j), dfs(i+1, j-w[i])+v[i]);
    return res;
}

         乍一看dfs好像就可以解決這個問題,那還有動態規劃什麼事。然而我們仔細分析一下時間複雜度,每一種狀態都用選或者不選兩種可能。所以我們可以得出使用dfs的時間複雜度為O(2^n)。顯然這個方法不是一個很好的方法,因為這個時間複雜度太高了。我們仔細研究可以發現,造成時間複雜度這麼高的原因是重複計算。既然我們找到複雜度這麼高的原因,那我們就可以想辦法減少它重複計算的次數。仔細分析容易想到,使用一個二維陣列來記錄每一次搜尋的答案,這樣我們就避免了重複計算。
//n,W 如題意所述
int W, n;
//w[i]和v[i]分別表示Wi,Vi
int w[MAXN], v[MAXN];
//儲存每一次搜尋的答案
//初始化dp陣列的值,使其全為-1
int dp[MAXN][MAXN];
//從第i個物品開始挑選總重量小於j的部分
int dfs(int i, int j){
    if(dp[i][j] >= 0) return dp[i][j];
    int res;
    //已經沒有剩餘物品
    if(i == n) res = 0;
    //無法挑選第i個物品
    else if(j < w[i]) res = dfs(i+1, j);
    //比較挑和不挑的情況,選取最大的情況
    else res = max(dfs(i+1, j), dfs(i+1, j-w[i])+v[i]);
    //將結果記錄在dp陣列中
    return dp[i][j] = res;
}



        這樣的小技巧,我們稱之為記憶化搜尋。我們只是小小的改變就讓它的時間複雜度降低至O(nW)。

        仔細分析,可以發現我們還可以有更簡單的寫法:

//dp[i+1][j] 表示從前i個物品挑選出總重量超過j的物品時,揹包中的最大價值
void solve(){
    //還沒開始挑選的時候,揹包裡的總價值為0
    for(int j = 0; j <= W; j++) dp[0][j] = 0;
    for(int i = 0; i < n; i++){
        for(int j = 0; j <= W; j++){
            if(j < w[i]) dp[i+1][j] = dp[i][j];
            else dp[i+1][j] = max(dp[i+1], dp[j-w[i]]+v[i]);
        }
    }
}

        使用遞推方程直接求解的方法,我們稱之為dp。因為他每一次的選取,都在動態的計算最優的情況。當然可能他區域性不是最優,但是整體一定是最優解。這就是他和貪心演算法最大的不同,貪心演算法,每一次都是最優,但是整體不一定不是最優。