揹包問題(0-1揹包+徹底揹包)

0-1揹包數組

有N件物品和一個容量爲V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可以使價值總和最大。

重要的點在於:每種物品僅有一件,能夠選擇放/不放網絡

子問題:f[i][v]表示前i件物品剛好放入一個 容量爲v 的揹包能夠得到的最大價值。優化

狀態轉移方程(遞推式):f[i][v]=max{f[i-1][v], f[i-1][v-c[i]]+w[i]};//考慮前i件物品放入這個子問題的時候,能夠轉化爲前i-1件物品已經放好。那麼若是放入第i件物品,那麼問題轉化爲 前i-1件物品放入剩餘容量爲v-c[i]的揹包裏;若是不放入第i件物品,那麼問題轉化爲 前i-1件物品放入剩餘容量爲v的揹包裏。spa

而若是放入第i件物品,那麼當前價值就是f[i-1][v-c[i]]+w[i]。所以當前最大價值就是 放入&不放入 之間的最大值。3d

 

能夠反向找到各類物品的選擇:從dp[N][V]開始,若是dp[i][j]=dp[i-1][j],則當前第i件物品沒有被選中,從dp[i-1][j]繼續找;不然,則表示選中,從dp[i-1][j-w[i]]開始找code

僞代碼:blog

int[][] dp=new int[N][V+1];
//初始化第一行
//僅考慮容量爲V的揹包放第0個物品,不放物品,價值爲0
for(int i=0;i<=V;i++){
     dp[0][i]=0;  
}
//初始化第一列
//容量爲0的揹包放物品,不放物品,價值爲0
for(int i=0;i<=N;i++){
dp[i][0]=0;
}
//根據狀態轉移方程,填充其餘行和列 for(int i=1;i<N;i++){ for(int j=1;j<=V;j++){
//裝不進去,當前剩餘空間小於第i個物品的大小
if(w[i]>j){
           dp[i][j]=dp[i-1][j];
}
//容量足夠,能夠放進去,比價值更大的方法

else
{ dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]); } } } //最後的結果 dp[N-1][V]

如何優化呢?那麼只能在空間複雜度上進行優化,只使用一維數組來存放結果遞歸

此時狀態轉移方程爲:f[v]=max{f[v], f[v-c[i]]};//那麼這個時候須要注意的是,在第二層循環中,須要使用從後往前計算,獲得結果。即要用一維數組記憶化的時候,須要用到當前位置的值 和 該位置以前的值。由於若是咱們須要計算f[4, 4]=max{f[3,4], f[3,1]}、f[4,3]=max{f[3,3], f[3,1]}、f[4,2]=max{f[3,2], f[3,1]}。class

若是是轉化爲一維數組,由於須要保證max中的f[v]是f[i-1][v],前面的f[v]是f[i][v]。也就是當前這一層的d[j]尚未被更新過,因此當前的d[j]用到的是i-1層的結果。若是從前日後計算,那麼下一次使用的d[j]是本層已經更新過的,會覆蓋掉i-1層的結果。循環

//解釋

因爲知道dp[i-1,1...j]就能夠獲得dp[i,j],下一層只須要根據上一層結果就能夠推出答案。

對於dp[j]=max{dp[j], dp[j-w[i]]+v[i]}而言,dp[j-w[i]]至關於二維的dp[i-1][j-w[i]],dp[j]是由前面的dp(1...j)推出來的。

所以好比從i=3推i=4,此時一維數組存放{0,0,2,4,4,6,6,6,7,7,9},這是i=3時全部子問題的解。若是從前日後推,那麼計算i=4時,

dp[0]=0, dp[1]=0, ... , (前面這幾項都放不進 w[i]=5的物品)dp[5]=max{dp[5], dp[5-5]+7}=7, dp[6]=max{dp[6], dp[6-5]+7}=7, dp[7]=max{dp[7], dp[7-5]+7}=9.....這裏會更新dp[5]、dp[6]...的值,那麼後續計算的時候 就沒辦法用到 上一輪循環時的 dp[5]、dp[6]....了(即 由於當前值 是由上一輪循環推出來的,若是從前日後,前一次循環保存下來的值 可能會被修改)就是我當前更新要用到這個值,可是這個值 在從前日後更新時,已經被修改了,那麼我用到的就是錯誤的值了。

 

初始化的話:(初始化 其實是 在沒有任何物品能夠放入揹包時 的合法狀態)

若是問法是「剛好裝滿」的最優解,那麼除了dp[0]初始化爲0,其餘都應該設置爲 負無窮大。這樣能保證最終的dp[V]爲剛好裝滿揹包時的最優解。此時,只有容量爲0的揹包 能夠在 什麼都不裝且價值爲0時被「剛好裝滿」,由於如dp[3]則表示,揹包容量爲3時,剛好裝滿的價值,此時沒有合法的解,所以屬於未定義狀態,設爲無窮大。

若是問法是「能夠不裝滿」的最優解,那麼全部的都應初始化爲0,由於「什麼都不裝」時,0就是合法解。

僞代碼:

int[] dp=new int[V+1];
//初始化第一行
//僅考慮容量爲V的揹包放第0個物品,不放物品,價值爲0
for(int i=0;i<=V;i++){
     dp[i]=w[0]<=i?v[0]:0;  
}

//根據狀態轉移方程,填充其餘行和列
for(int i=1;i<N;i++){
    for(int j=V;j>=w[i];j--){
            dp[j]=Math.max(dp[j],dp[j-w[i]]+v[i]);

    }
}

//最後的結果
dp[V]

例題:給定一個僅包含正整數的非空數組,肯定該數組是否能夠分紅兩部分,要求兩部分的和相等

思路:即給定N個元素組成的數組arr,數組元素的和爲sum。轉換成揹包問題,每一個物品的重量和價值爲arr[i],兩部分和相等,即揹包的限重爲sum/2.

if(nums==null || nums.length==0){
     return true;
}
int sum=0;
for(int num : nums){
     sum+=num;
}
//若是sum不能夠平分,那麼就不可分爲兩塊
if(sum%2!=0){
     return false;
}
sum/=2;
//定義
boolean[] dp=new boolean[sum+1];
//初始化
dp[0]=true; for(int i=1; i<=nums.length; i++){
//爲何要從後往前更新dp,由於每一個位置 依賴於 前面一個位置 加上 nums[i]。若是從前日後更新的話,
那麼dp[i-2]會影響dp[i-1],而後dp[i-1]會影響dp[i],即一樣的一個nums[i]被反覆使用了屢次。
for(int j=sum; j>=nums[i]; j--){ dp[j]=dp[j] || dp[j-nums[i]]; } } //輸出 dp[sum]

 

徹底揹包

有N種物品和一個容量爲V的揹包,每種物品都有無限件可用。第i種物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可以使這些物品的費用總和不超過揹包容量,且價值總和最大。

重要的區別在於徹底揹包是 每種無限件

狀態轉移方程:f[i][j]=Math.max(f[i-1][j-k*c[i]]+k*w[i]),0<=k*c[i]<=j;//根據第i件物品放多少件,即前i-1件物品中 選擇 若干件 放入剩餘的空間上,使得最大。(f[i][j]表示 前i種物品 放入一個容量爲j的揹包種得到 最大價值)

//遞歸和動態規劃的區別:動態規劃多使用了一個二維數組來存儲中間的解

代碼:

int[][] dp=new int[N][V+1];
//初始化第一行
//僅考慮容量爲V的揹包放第0個物品,不放物品,價值爲0
for(int i=0;i<=V;i++){
     dp[0][i]=0;  
}
//初始化第一列
//容量爲0的揹包放物品,不放物品,價值爲0
for(int i=0;i<=N;i++){
     dp[i][0]=0;
}
//根據狀態轉移方程,填充其餘行和列
for(int i=1;i<N;i++){
    for(int j=1;j<=V;j++){
           //裝不進去,當前剩餘空間小於第i個物品的大小
           if(w[i]>j){
                dp[i][j]=dp[i-1][j];
           }
           //容量足夠,能夠放進去,比價值更大的方法。取k個物品i,再k種選擇 選出 最優解
           else{
                 for(int k=0; k*w[i]<=j; k++){
                        dp[i][j]=Math.max(dp[i-1][j], dp[i-1][j-w[i]*k])+v[i]*k;
                 }                 
           }    
    }
}

//最後的結果
dp[N-1][V]

一樣使用一維數組 來優化 空間複雜度

dp[i]=Math.max(dp[i], dp[i-w[i]]+v[i])

int[] dp=new int[V+1];
//初始化第一行
//僅考慮容量爲V的揹包放第0個物品,不放物品,價值爲0
for(int i=0;i<=V;i++){
     dp[i]=0;  
}

//根據狀態轉移方程,填充其餘行和列
for(int i=1;i<N;i++){
    for(int j=w[i];j<=V;j++){
            dp[j]=Math.max(dp[j],dp[j-w[i]]+v[i]);

    }
}

//最後的結果
dp[V]

(示意圖源於網絡,侵刪)

相關文章
相關標籤/搜索