1. 程式人生 > >【動態規劃】背包問題

【動態規劃】背包問題

urn 兩個 pro 數組實現 可以轉化 轉化 題目 int 遞增

  背包問題無疑是最經典的dp問題,其次就是關於字符串匹配問題,數組最長遞增(減)序列長度等等。背包問題變體很多。

  動態規劃問題實際上與備忘錄式深搜有些類似。

1. 0-1背包

題目:

  有n個重量和價值分別為wi, vi的物品。從這些物品中挑選出總重量不超過W的物品,求所有挑選方案中價值總和的最大值。

限制條件:

1<= n <= 100; 1<= wi, vi <= 100; 1 <= W <= 1000

樣例:

輸入

n = 4
(w, v) = {(2,3),(1,2),(3,4),(2,2)}
W = 5

輸出

7 (選擇第0,1,3號物品)

  先從深搜開始,仔細分析問題,就會發現一個特點:每種物品有兩種選擇,放或者不放。

int n, int W;
int w[MAX_N], v[MAX_N];
int process(int i, int j){
    int res;
    if(i == n){
        res = 0;
    } else if(j < w[i]){
        res = process(i + 1, j);
    } else {
        res = max(process(i + 1, j), process(i + 1, j - w[i]) + v[i]);
    }
    return res;
}
void solve(){
    printf("%d\n", process(0, W));
}

  

  深搜的一個缺點就是會重復計算,所以有了備忘錄式深搜,剪枝操作減少不必要的計算。

int n, int W;
int w[MAX_N], v[MAX_N];
int dp[MAX_N][MAX_W + 1];

int process(int i, int j){
    if(dp[i][j] >= 0) {
        return dp[i][j];
    }  
    int res;
    if(i == n){
        res = 0;
    } else if(j < w[i]){
        res = process(i + 1, j);
    } else {
        res = max(process(i + 1, j), process(i + 1, j - w[i]) + v[i]);
    }
    return dp[i][j] = res;
}
void solve(){
  memset(dp, -1, sizeof(dp));
  printf("%d\n", process(0, W));
}

  

  還有一種深搜寫法:

int process(int i, int j, int sum){
    int res;
    if(i == n){
        res = sum;
    } else if(j < w[i]){
        res = process(i + 1, j, sum);
    } else {
        res = max(process(i + 1, j, sum), process(i + 1, j - w[i], sum + v[i]);
    }
    return res;
}

  

  這種寫法不利於備忘錄式搜索的實現,盡量不要用這種形式。

  根據備忘錄式深搜,dp[i][j]為從第i個物品開始挑選總重小於j時,總價值的最大值。

  i = n;  dp[n][j] = 0;

  j < w[j] dp[i + 1][j]

其他   max(dp[i + 1][j], dp[i + 1][j - w[i] + v[i])

void solve(){
    for (int i = n - 1; i >= 0; --i){
        for(int j = 0; j <= W; ++j){
            if(j < w[i]){
                dp[i][j] = dp[i + 1][j];
            } else {
                dp[i][j] = max(dp[i + 1][j], dp[i + 1][j - w[i]] + v[i]);
            }
        }
    }
    printf("%d\n", dp[0][W]);
}

 

  重新定義dp方式:dp[i + 1][j] 為 從前面i個物品中選出總重量不超過j的物品時總價值的最大值。

  dp[0][j] = 0;

  dp[i + 1][j] = dp[i][j]            j < w[i]

        max(dp[i][j], dp[i][j - w[i]] + v[i]) 其他

  

void solve(){
    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][j], dp[i][j - w[i]] + v[i]);
            }
        }
    }
    printf("%d\n", dp[n][W]);
}

  

  背包問題的基本方程式: dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);

  動態規劃只與之前的狀態有關,不會和下一個狀態有聯系。對於此問題的狀態定義,dp[i + 1][j]為將前i件物品放入容量為j的背包,即問題的子問題。若只考慮第i件物品的策略(放或不放),那麽就可以轉化為一個只和前i - 1件物品相關的問題。如果不放第i件物品,那麽問題就轉化為“前i - 1件物品放入容量為j的背包中”,價值為dp[i - 1][j];如果放第i件物品,那麽問就轉化為“前i - 1件物品放入剩下的容量為j - w[i] 的背包中, 然後將第i件物品放入背包中”,此時能獲得的最大價值就是dp[i][j - w[i]]再加上通過放入第i件物品獲得的價值v[i],即dp[i][j - w[i]] + v[i] 。

  0-1背包問題解題思路就是如此,但是在具體的代碼實現中可以優化代碼。

  空間復雜度優化:分析dp方程:dp[i+1][a]的計算只來自dp[i][b](a >= b),那麽就聯想到數組復用。使用一維數組實現代碼。dp[i + 1][j]由dp[i][j]和dp[i][j - w[i]]兩個子問題推導得到,所以要保證在推dp[i + 1][j]時能夠取用dp[i][j]和dp[i][j - w[i]]的值。也就是說,推導dp[j]時要使用上一循環的dp[j]和dp[j - w[i]],那麽就必須保證在本次循環推導dp[j]時不能改寫dp[j]和dp[j - w[i]]。

int dp[MAX_W + 1];
void solve(){
    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]);
        }
    }
    printf("%d\n", dp[n][W]);
}

  

  這裏dp[j - w[i]]對應著原來的dp[i][j - w[i]],如果將j的循環順序顛倒,即w[i]-W,那麽就成了dp[i+1][j]由dp[i][j]推導得到,與題意不符。

【動態規劃】背包問題