動態規劃---01揹包與記憶化搜尋
阿新 • • 發佈:2018-12-23
動態規劃是一種高效的演算法。在數學和電腦科學中,是一種將複雜問題的分成多個簡單的小問題思想 ---- 分而治之。因此我們使用動態規劃的時候,原問題必須是重疊的子問題。運用動態規劃設計的演算法比一般樸素演算法高效很多,因為動態規劃不會重複計算已經計算過的子問題。因為動態規劃又可以稱為“記憶化搜尋”。
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。因為他每一次的選取,都在動態的計算最優的情況。當然可能他區域性不是最優,但是整體一定是最優解。這就是他和貪心演算法最大的不同,貪心演算法,每一次都是最優,但是整體不一定不是最優。