引言
“01背包問題”是一個比較基礎的算法問題,它可以衍生為其他相對復雜的算法問題,比如“裝箱問題”。通過對“01背包問題”的學習與破解,我們可以掌握一些比較常見的算法,配養一定的算法思維以及解題能力。
“01背包問題”是學習計算機相關專業的學生必須掌握的經典算法問題之一。
1、“01背包問題”的問題雛形
01背包是在M件物品取出若干件放在空間為W的背包里,每件物品的體積為W1,W2至Wn,與之相對應的價值為P1,P2至Pn。
01背包是背包問題中最簡單的問題。01背包的約束條件是給定幾種物品,每種物品有且只有一個,並且有權值和體積兩個屬性。在01背包問題中,因為每種物品只有一個,對於每個物品只需要考慮選與不選兩種情況。
如果不選擇將其放入背包中,則不需要處理。如果選擇將其放入背包中,由於不清楚之前放入的物品占據了多大的空間,需要枚舉將這個物品放入背包后可能占據背包空間的所有情況。
2、遞歸算法解“01背包問題”
2.1 算法分析
01背包問題與部分背包問題很類似,但是部分背包問題是物品是可以進行分割的,假如不能夠拿取當前的物品,那么可以拿取這個物品的某一部分來構成最大質量並且價值是最大的。
但是01背包問題中物品是不可以進行分割的,可以選擇拿取當前的物品也可以選擇不拿這兩種情況,所以這個是兩個問題的區別。
我們先不考慮怎么樣寫計算機程序,先是看自己的直覺思維,怎么樣拿取物品才能夠使拿取的物品最終的價值最大但是不超過最大質量。
設現有4個質量w分別為2、1、3、2,價值v分別為3、2、4、2的物品,要放入承重總量為W的背包中。
首先我們可能考慮先湊成目標的最大質量,W = 5 那么我們可以由多個組合來拿取,可以拿取0,1, 3號物品,0,2號物品,也可以拿取2,3號物品,再在這幾個組合中找出價值最大的,可以發現0,1, 3號物品和2,3號物品的價值是最大的。
但是除了可以湊成目標最大質量情況有可能價值最大,也可能是這樣的情況:不能夠湊成最大質量,但是它的總價值有可能是最大的。
所以,我們可以使用遞歸的方式來進行解決,在所有的可能中找出一個能夠不超過目標質量但是價值是最大的,找出所有可能這實際上也是一種深度優先搜索的過程。
我們在遇到問題的時候要先進行給出測試樣例的分析,利用簡單的推導,類比,找出問題的相似性來進行問題的求解。
下面是使用普通遞歸的方法來進行解決(也可以叫做深搜來解決:實際上就是深搜)。
其中遞歸有兩個變量在進行變化一個是可以選擇的當前物品的下標,一個是可以選擇的當前物品的最大質量,所以在方法中傳入兩個參數來,還有其他的不變的參數也可以傳遞進來方便操作,也可以將這些設置為全局變量不用傳遞進來都可以。
2.2 算法描述
要解決01背包問題,就必須要做對比。
首先我們要確定遞歸的出口,當物品數量為0的時候,或者當書包容量為0,就不能再裝貨物了,這就是遞歸的出口。
接下來,我們要判斷第i件物品的重量是不是大於當前的物品重量,如果大於,則不挑選該物品,小於則挑選,繼續向下進行遞歸,直至滿足遞歸出口條件。
2.3 代碼實現
import java.util.*;
public class Main{
/**
*
* @param i 第i件物品
* @param j 書包的重量
* @param V 物品的價值
* @param W 物品的重量
* @return
*/
public static int dfs(int i, int j, int[] W, int[] V) {
// 檢查物品個數是否小於等於0,書包容量是否足夠容納第i件物品
if (i <= 0 || j == 0) {
return 0;
}
if (W[i] > j) {
return dfs(i - 1, j, W, V);
}
int value, value2 = 0;
// 不選擇第i件物品,則遞歸挑選除去第i件物品的剩余物品
value = dfs(i - 1, j, W, V);
// 選擇第i件物品,同時要減去對映的背包空間,且要加上物品i的價格V[i]
value2 = dfs(i - 1, j - W[i], W, V) + V[i];
// 如果不選擇第i件物品的遞歸返回結果大於選擇第i件的遞歸返回結果,則返回不選擇第i件的結果
if (value > value2) {
return value;
}
// 反之返回選擇第i件物品的結果
return value2;
}
public static void main(String[] args) {
// 物品金額,0相當於開頭位置的作用,規范數組下標
int[] V = { 0, 3, 4, 5, 6, 10 };
// 物品的總重量,0相當於開頭位置的作用,規范數組下標
int[] W = { 0, 2, 3, 4, 5, 9 };
// 假設書包容量是20
System.out.println(dfs(5, 20, W, V));
}
}
3、動態規划法解“01背包問題”
3.1 算法分析
動態規划與分治法類似,都是把大問題拆分成小問題,通過尋找大問題與小問題的遞推關系,解決一個個小問題,最終達到解決原問題的效果。
但不同的是,分治法在子問題和子子問題等上被重復計算了很多次,而動態規划則具有記憶性,通過填寫表把所有已經解決的子問題答案紀錄下來,在新問題里需要用到的子問題可以直接提取,避免了重復計算,從而節約了時間,所以在問題滿足最優性原理之后,用動態規划解決問題的核心就在於填表,表填寫完畢,最優解也就找到。
3.2 算法描述
聲明一個大小為m[n][c]的二維數組,m[i][j]表示:在面對第i件物品,且背包容量為j時所能獲得的最大價值。那么我們可以很容易分析得出 m[i][j] 的計算方法:
(1)j < w[i]的情況,這時候背包容量不足以放下第i件物品,只能選擇不拿:m[i][j] = m[i-1][j]
(2)j>=w[i] 的情況,這時背包容量可以放下第i件物品,我們就要考慮拿這件物品是否能獲取更大的價值。
如果拿取,m[i][j] = m[i-1][j-w[i]] + v[i]。 這里的m[i-1][j-w[i]]指的就是考慮了i-1件物品,背包容量為j-w[i]時的最大價值,也是相當於為第i件物品騰出了w[i]的空間。
如果不拿,m[i][j] = m[i-1][j] , 同(1)
究竟是拿還是不拿,自然是比較這兩種情況那種價值最大。
由此可以得到狀態轉移方程:
if(j>=w[i]){
m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);}
else{
m[i][j]=m[i-1][j];}
3.3 代碼實現
import java.util.*;
/**
* 運用動態規划解決
*
*/
public class Main {
public static void main(String[] args) {
//背包總重量
int bag_weight=20;
//物品個數
int shopping_num=5;
//物品金額
int []V={0,3,4,5,6,10};
//物品的重量
int []W={0,2,3,4,5,9};
//定義背包容量與物品個數的狀態數組 bag[i][j];i表示當前有i件,j表示當前的容量
int [][]bag=new int[shopping_num+1][bag_weight+1];
//依次選擇第i件物品
for(int i=1;i<=shopping_num;i++){
//依次的增加背包容量數目
for(int j=1;j<=bag_weight;j++){
if(i>0&&W[i]<=j){
bag[i][j]=(bag[i-1][j]>bag[i-1][j-W[i]]+V[i]?bag[i-1][j]:bag[i-1][j-W[i]]+V[i]);
}
else{
bag[i][j]=bag[i-1][j];
}
}
}
System.out.println(bag[shopping_num][bag_weight]);
}
}