問題描述:
0-1背包問題:給定n種物品和一背包。物品 i 的重量似乎 wi,其價值為 vi,背包的容量為 c。問應該如何選擇裝入背包中的物品,使得裝入背包中物品的總價值最大?
說實在的,書上講的東西生澀難懂,我更偏向于看一些有趣的東西。我們來換一個風格來描述這一個問題。
以下內容大部分來自《算法圖解》一書。看完之后大有收獲。
另一種風格的描述:
假設你是一個小偷,背著一個可裝下4磅東西的背包,你可以偷竊的物品如下:
為了讓偷竊的商品價值最高,你該選擇哪些商品?
簡單算法
最簡單的算法是:嘗試各種可能的商品組合,并找出價值最高的組合。
這樣顯然是可行的,但是速度非常慢。在只有3件商品的情況下,你需要計算8個不同的集合;當有4件商品的時候,你需要計算16個不同的集合。每增加一件商品,需要計算的集合數都將翻倍!這種算法的運行時間是O(2?),真的是慢如蝸牛。
動態規劃
解決這樣問題的答案就是使用動態規劃!下面來看看動態規劃的工作原理。動態規劃先解決子問題,再逐步解決大問題。
對于背包問題,你先解決小背包(子背包)問題,再逐步解決原來的問題。
比較有趣的一句話是:每個動態規劃都從一個網格開始。
背包問題的網格如下:
網格的各行為商品,各列為不同容量(1~4磅)的背包。所有這些列你都需要,因為它們將幫助你計算子背包的價值。
網格最初是空的。你將填充其中的每個單元格,網格填滿后,就找到了問題的答案!
1.吉他行
后面會列出計算這個網格中單元格值得公式,但現在我們先來一步一步做。首先來看第一行。
這是吉他行,意味著你將嘗試將吉他裝入背包。在每個單元格,都需要做一個簡單的決定:偷不偷吉他?別忘了,你要找出一個價值最高的商品集合。
第一個單元格表示背包的的容量為1磅。吉他的重量也是1磅,這意味著它能裝入背包!因此這個單元格包含吉他,價值為1500美元。
下面來填充網格。
與這個單元格一樣,每個單元格都將包含當前可裝入背包的所有商品。
來看下一個單元格。這個單元格表示背包容量為2磅,完全能夠裝下吉他!
這行的其他單元格也一樣。別忘了,這是第一行,只有吉他可供你選擇,換而言之,你假裝現在還沒發偷竊其他兩件商品。
此時你很可能心存疑惑:原來的問題說的額是4磅的背包,我們為何要考慮容量為1磅、2磅等得背包呢?前面說過,動態規劃從小問題著手,逐步解決大問題。這里解決的子問題將幫助你解決大問題。
別忘了,你要做的是讓背包中商品的價值最大。這行表示的是當前的最大價值。它指出,如果你有一個容量4磅的背包,可在其中裝入的商品的最大價值為1500美元。
你知道這不是最終解。隨著算法往下執行,你將逐步修改最大價值。
2.音響行
我們來填充下一行——音響行。你現在處于第二行,可以偷竊的商品有吉他和音響。
我們先來看第一個單元格,它表示容量為1磅的背包。在此之前,可裝入1磅背包的商品最大價值為1500美元。
該不該偷音響呢?
背包的容量為1磅,顯然不能裝下音響。由于容量為1磅的背包裝不下音響,因此最大價值依然是1500美元。
接下來的兩個單元格的情況與此相同。在這些單元格中,背包的容量分別為2磅和3磅,而以前的最大價值為1500美元。由于這些背包裝不下音響,因此最大的價值保持不變。
背包容量為4磅呢?終于能夠裝下音響了!原來最大價值為1500美元,但如果在背包中裝入音響而不是吉他,價值將為3000美元!因此還是偷音響吧。
你更新了最大價值。如果背包的容量為4磅,就能裝入價值至少3000美元的商品。在這個網格中,你逐步地更新最大價值。
3.筆記本電腦行
下面以同樣的方式處理筆記本電腦。筆記本電腦重3磅,沒法將其裝入1磅或者2磅的背包,因此前兩個單元格的最大價值仍然是1500美元。
對于容量為3磅的背包,原來的最大價值為1500美元,但現在你可以選擇偷竊價值2000美元的筆記本電腦而不是吉他,這樣新的最大價值將為2000美元。
對于容量為4磅的背包,情況很有趣。這是非常重要的部分。當前的最大價值為3000美元,你可不偷音響,而偷筆記本電腦,但它只值2000美元。
價值沒有原來高,但是等一等,筆記本電腦的重量只有3磅,背包還有1磅的重量沒用!
在1磅的容量中,可裝入的商品的最大價值是多少呢?你之前計算過。
根據之前計算的最大價值可知,在1磅的容量中可裝入吉他,價值1500美元。因此,你需要做如下的比較:
你可能始終心存疑惑:為何計算小背包可裝入的商品的最大價值呢?但愿你現在明白了其中的原因!余下了空間時,你可根據這些子問題的答案來確定余下的空間可裝入哪些商品。筆記本電腦和吉他的總價值為3500美元,因此偷它們是更好的選擇。
最終的網格類似于下面這樣。
答案如下:將吉他和筆記本電腦裝入背包時價值更高,為3500美元。
你可能認為,計算最后一個單元格的價值時,我使用了不同的公式。那是因為填充之前的單元格時,我故意避開了一些復雜的因素。其實,計算每個單元格的價值時,使用的公式都相同。這個公式如下。
你可以使用這個公式來計算每個單元格的價值,最終的網格將與前一個網格相同。現在你明白了為何要求解子問題了吧?你可以合并兩個子問題的解來得到更大問題的解。
代碼實現:
算法的核心是思想,當清楚了整個過程,那么寫代碼就簡單了,直接來模擬上述的一個過程:
/**
* @author:我沒有三顆心臟
* @create:2017-11-14-10:24
*/
public class MaxBag {
static int n; // 描述物品個數
static int c; // 描述背包容量
static int[] value; // 描述物品價值
static int[] weight; // 描述物品重量
public static void main(String[] args) {
// 初始賦值操作
value = new int[]{1500, 3000, 2000};
weight = new int[]{1, 4, 3};
c = 4;
n = 3;
// 構造最優解的網格:3行4列
int[][] maxValue = new int[n][c];
for (int i = 0; i < n; i++) {
for (int j = 0; j < c; j++) {
maxValue[i][j] = 0;
}
} // end for
// 填充網格
for (int i = 0; i < n; i++) {
for (int j = 1; j <= c; j++) {
if (i == 0) {
maxValue[i][j - 1] = (weight[i] <= j ? value[i] : 0);
} else {
int topValue = maxValue[i - 1][j - 1]; // 上一個網格的值
int thisValue = (weight[i] <= j ? // 當前商品的價值 + 剩余空間的價值
(j - weight[i] > 0 ? value[i] + maxValue[i - 1][j - weight[i]] : value[i])
: topValue);
// 返回 topValue和thisValue中較大的一個
maxValue[i][j - 1] = (topValue > thisValue ? topValue : thisValue);
} // end if
} // end inner for
} // end outer for
// 打印結果二維數組maxValue
for (int i = 0; i < n; i++) {
for (int j = 0; j < c; j++) {
System.out.printf("%6d", maxValue[i][j]);
}
System.out.println();
}
}
}
最后打印出來的結果如下:
再增加一件商品將如何呢
假設你發現還有第四件商品可偷——一個iPhone!(或許你會毫不猶豫的拿走,但是請別忘了問題的本身是要拿走價值最大的商品)
此時需要重新執行前面所做的計算嗎?不需要。別忘了,動態規劃逐步計算最大價值。到目前為止,計算出的最大價值如下:
這意味著背包容量為4磅時,你最多可偷價值3500美元的商品。但這是以前的情況,下面再添加表示iPhone的行。
我們還是從第一個單元格開始。iPhone可裝入容量為1磅的背包。之前的最大價值為1500美元,但iPhone價值2000美元,因此該偷iPhone而不是吉他。
在下一個單元格中,你可裝入iPhone和吉他。
對于第三個單元格,也沒有比裝入iPhone和吉他更好的選擇了。
對于最后一個單元格,情況比較有趣。當前的最大價值為3500美元,但你可以偷iPhone,這將余下3磅的容量。
3磅容量的最大價值為2000美元!再加上iPhone價值2000美元,總價值為4000美元。新的最大價值誕生了!
最終的網格如下。
問題:沿著一列往下走,最大價值可能降低嗎?
答案是:不可能。因為每次迭代時,你都存儲的是當前的最大價值。最大價值不可能比以前低!
總結:
有時候教科書生澀難懂,你需要找一些更好的資料來幫助你學習,更重要的一點是,保持一顆好奇心還有求知欲。
歡迎轉載,轉載請注明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微信號:wmyskxz_javaweb
分享自己的Java Web學習之路以及各種Java學習資料