背包必备理论基础1
整體背包重點是01背包和完全背包。
而完全背包又是也是01背包稍作變化而來,即:完全背包的物品數量是無限的。
所以背包問題的理論基礎重中之重是01背包
下面舉一個例子:
背包最大重量為4。
物品 重量 價值
物品0 1 15
物品1 3 20
物品2 4 30
問背包能背的物品最大價值是多少?
二維dp數組01背包
確定dp數組以及下標的含義
對于背包問題,有一種寫法, 是使用二維數組,即dp[i][j]
表示從下標為[0-i]的物品里任意取,放進容量為j的背包,價值總和最大是多少。
確定遞推公式
根據dp[i][j]的含義:從下標為[0-i]的物品里任意取,放進容量為j的背包,價值總和最大是多少。
那么可以有兩個方向推出來dp[i][j]、
1、 由dp[i - 1][j]推出,即背包容量為j,里面不放物品i的最大價值,此時dp[i][j]就是dp[i - 1][j] 。(背包放不下物品i了?)
2、由dp[i -1][j - weight[i]]推出,意思是當背包容量為j -weight[i]的時候不放物品i的最大價值,那么dp[i - 1][j - weight[i]] + value[i](物品i的價值),就是背包放物品i得到的最大價值 。(背包可以放下物品i,看看剩余容量還能裝下價值為多少的物品)
3、所以遞歸公式: dp[i][j] = max(dp[i - 1][j], dp[i -1][j - weight[i]] + value[i]);
dp數組如何初始化
關于初始化,一定要和dp數組的定義吻合,否則到遞推公式的時候就會越來越亂。
首先從dp[i][j]的定義出發,如果背包容量j為0的話,即dp[i][0],無論是選取哪些物品,背包價值總和一定為0。如圖:
在看其他情況。
由狀態轉移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
可以看出i 是由 i-1 推導出來,那么i為0的時候就一定要初始化。
dp[0][j],即:i為0,存放編號0的物品的時候,各個容量的背包所能存放的最大價值。
代碼如下:
for(int ii=0;ii<m;ii++){dp[ii][0]=0; } for(int jj=weight[0];jj<=bagWeight;jj++){dp[0][jj]=value[0]; }
dp[0][j] 和 dp[i][0] 都已經初始化了,那么其他下標應該初始化多少呢?
dp[i][j]在推導的時候一定是取價值最大的數,如果題目給的價值都是正整數那么非0下標都初始化為0就可以了,因為0就是最小的了,不會影響取最大價值的結果。
如果題目給的價值有負數,那么非0下標就要初始化為負無窮了。例如:一個物品的價值是-2,但對應的位置依然初始化為0,那么取最大值的時候,就會取0而不是-2了,所以要初始化為負無窮。
而背包問題的物品價值都是正整數,所以初始化為0就可以了。
這樣才能讓dp數組在遞歸公式的過程中取最大的價值,而不是被初始值覆蓋了。
如圖:
確定遍歷順序
在如下圖中,可以看出,有兩個遍歷的維度:物品與背包重量
那么問題來了,先遍歷 物品還是先遍歷背包重量呢?
其實都可以!! 但是先遍歷物品更好理解。
那么我先給出先遍歷物品,然后遍歷背包重量的代碼。
// weight數組的大小 就是物品個數 for(int ii=1;ii<=weight.size();ii++){for(int jj=0;jj<=bagWeight;jj++){// 遍歷背包容量// 這個是為了展現dp數組里元素的變化,就是說當前背包容量小于當前物品重量//直白點就是當前背包裝不下當前物品if (j < weight[i]) dp[i][j] = dp[i - 1][j]; //1、背包空間足以容納當前物品和其他物品;(價值疊加)//2、背包容納當前物品但是剩余空間不能容納其他物品;(當前物品價值高)//3、背包可以容納其他物品,但剩余空間容不下當前物品;(當前物品價值低)else dp[ii][jj]=max(dp[ii-1][jj],dp[ii-1][jj-weight[ii]]+value[ii]);} }舉例推導dp數組
來看一下對應的dp數組的數值,如圖:
最終結果就是dp[2][4]。
做動態規劃的題目,最好的過程就是自己在紙上舉一個例子把對應的dp數組的數值推導一下,然后在動手寫代碼!
測試代碼:
void test_2_wei_bag_problem1() {vector<int> weight = {1, 3, 4};vector<int> value = {15, 20, 30};int bagWeight = 4;// 二維數組vector<vector<int>> dp(weight.size() + 1, vector<int>(bagWeight + 1, 0));// 初始化for (int j = weight[0]; j <= bagWeight; j++) {dp[0][j] = value[0];}// weight數組的大小 就是物品個數for(int i = 1; i < weight.size(); i++) { // 遍歷物品for(int j = 0; j <= bagWeight; j++) { // 遍歷背包容量if (j < weight[i]) dp[i][j] = dp[i - 1][j];else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);}}cout << dp[weight.size() - 1][bagWeight] << endl; }int main() {test_2_wei_bag_problem1(); }動態規劃(英語:Dynamic programming,簡稱 DP)是一種在數學、管理科學、計算機科學、經濟學和生物信息學中使用的,通過把原問題分解為相對簡單的子問題的方式求解復雜問題的方法。
動態規劃常常適用于有重疊子問題和最優子結構性質的問題,并且記錄所有子問題的結果,因此動態規劃方法所耗時間往往遠少于樸素解法。
動態規劃有自底向上和自頂向下兩種解決問題的方式。自頂向下即記憶化遞歸,自底向上就是遞推。
使用動態規劃解決的問題有個明顯的特點,一旦一個子問題的求解得到結果,以后的計算過程就不會修改它,這樣的特點叫做無后效性,求解問題的過程形成了一張有向無環圖。動態規劃只解決每個子問題一次,具有天然剪枝的功能,從而減少計算量。
總結