【動態規劃】背包問題
背包問題無疑是最經典的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]推導得到,與題意不符。
【動態規劃】背包問題