導語:java
揹包問題能夠說是一個老生常談的問題,一般被用做面試題來考查面試者對動歸的理解,咱們常常說學算法,初學者最難理解的就是 「二歸」,一個叫遞歸,另外一個叫動歸。揹包問題屬於特殊的一類動歸問題,也就是按值動歸,這篇文章我會列舉一些常見的揹包問題,涵蓋 0-1 揹包,徹底揹包,以及 多重揹包。我同時會分享一些經典的題目幫助理解其中的思路與解題技巧。本文是參考了網上廣爲人知的 「揹包九講」 一書,文末會有下載連接。git
一般揹包這一類題目,題目大概就是給你一個容量或者大小固定的揹包,而後要求你去用這個揹包去裝物品,通常來講這些物品都是大小固定的,可是題目對物品的限定不一樣,衍生出來多種揹包問題,例如 0-1 揹包 問題中,物品個數有且僅有一個;徹底揹包 問題中的物品個數是無限的;多重揹包 問題中的針對不一樣的物品,個數不同。一般題目會要你求出揹包能裝的最大價值(每一個物品都會有容量和價值),固然也會有不同的問法,相似揹包可否被裝滿,還有揹包能裝的最大容量是多少,多少種方式填滿揹包。可是這些並非揹包問題的全部,還有 分組揹包 問題,依賴揹包 問題等等,由於考慮到這篇文章主要是針對面試,而不是競賽,這些有機會再去介紹。github
題目:
面試
有N件物品和一個容量爲V的揹包。放入第i件物品耗費的費用是C[i] ,獲得的價值是W[i]。求解將哪些物品裝入揹包可以使價值總和最大。求出最大總價值算法
分析:
數組
對於每個物品能夠考慮放,或者不放;若是當前是第 i 個物品,當前揹包裏面物品總價值是,揹包當前容量是
,若是取這個物品,揹包總價值會變成
,揹包容量會變成
。以前咱們提到過,揹包是屬於按值動歸,咱們把揹包劃分爲 1-V 個區間,也就是揹包全部可能的大小,而後針對全部的物品,看看每一個揹包容量下能存放的最大價值,代碼以下:app
public static int zeroOnePack(int V, int[] C, int[] W) {
// 防止無效輸入
if ((V <= 0) || (C.length != W.length)) {
return 0;
}
int n = C.length;
// dp[i][j]: 對於下標爲 0~i 的物品,揹包容量爲 j 時的最大價值
int[][] dp = new int[n + 1][V + 1];
// 揹包空的狀況下,價值爲 0
dp[0][0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= V; ++j) {
// 不選物品 i 的話,當前價值就是取到前一個物品的最大價值,也就是 dp[i - 1][j]
dp[i][j] = dp[i - 1][j];
// 若是選擇物品 i 使得當前價值相對不選更大,那就選取 i,更新當前最大價值
if ((j >= C[i - 1]) && (dp[i][j] < dp[i - 1][j - C[i - 1]] + W[i - 1])) {
dp[i][j] = dp[i - 1][j - C[i - 1]] + W[i - 1];
}
}
}
// 返回,對於全部物品(0~N),揹包容量爲 V 時的最大價值
return dp[n][V];
}
複製代碼
優化:
less
空間優化:
ide
僅僅看代碼就能夠發現,其實 dp 數組當前行的計算只用到了前一行,咱們能夠利用 滾動數組 來優化,可是再仔細看下去的話,你就會發現其實還能夠更優,當前行的遍歷用到的值是上一行的前面列的值,若是咱們第二層 for 循環遍歷的時候倒着遍歷的話,保證了前面更新的值不會被新計算的值覆蓋掉,咱們僅僅用一維數組就能夠完美解決問題,代碼以下:優化
public static int zeroOnePackOpt(int V, int[] C, int[] W) {
// 防止無效輸入
if ((V <= 0) || (C.length != W.length)) {
return 0;
}
int n = C.length;
int[] dp = new int[V + 1];
// 揹包空的狀況下,價值爲 0
dp[0] = 0;
for (int i = 0; i < n; ++i) {
for (int j = V; j >= C[i]; --j) {
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
return dp[V];
}
複製代碼
極端狀況優化:
當揹包的 V 特別大的時候,對於每個物品都去遍歷一遍沒有意義,經過閾值來進行優化,優化的同時能夠考慮將數組從大到小排個序:
public static int zeroOnePackOpt(int V, int[] C, int[] W) {
// 防止無效輸入
if ((V <= 0) || (C.length != W.length)) {
return 0;
}
int n = C.length;
int[] dp = new int[V + 1];
int bound, sum = 0, total = 0;
for (int i : C) {
total += i;
}
for (int i = 0; i < n; ++i) {
bound = Math.max(V - total + sum, C[i]);
sum += C[i];
for (int j = V; j >= bound; --j) {
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
return dp[V];
}
複製代碼
0-1 揹包 基本概況就是這些,固然可能問題的問法會不同,例如:
揹包能不能被裝滿
解題思路就是將 int 數組換成 boolean 數組,也不用去考慮物品的價值來,直接看容量夠不夠,能不能裝進揹包便可
揹包能裝的最大容量
也很簡單,解法和上面 「揹包能不能被裝滿」 同樣,只不過最後須要從後往前遍歷 dp 數組,直到找到 true
多少種方式塞滿揹包
一樣是不用考慮物品的價值,用 int 數組,可是裏面記錄的是個數,揹包被填充的個數,也就是把這裏的個數看成價值來看待,只不過 W[i] = 1。
下面是以前作過的一些關於 0-1 揹包 的題目,我會給出相應的思路,能夠試着寫寫,但願對你有幫助:
1、Need A offer
Speakless很早就想出國,如今他已經考完了全部須要的考試,準備了全部要準備的材料,因而,便須要去申請學校了。要申請國外的任何大學,你都要交納必定的申請費用,這但是很驚人的。Speakless沒有多少錢,總共只攢了n萬美圓。他將在m個學校中選擇若干的(固然要在他的經濟承受範圍內)。每一個學校都有不一樣的申請費用a(萬美圓),而且Speakless 估計了他獲得這個學校offer的可能性b。不一樣學校之間是否獲得offer不會互相影響。「I NEED A OFFER」,他大叫一聲。幫幫這個可憐的人吧,幫助他計算一下,他能夠收到至少一份offer的最大機率。(若是Speakless選擇了多個學校,獲得任意一個學校的offer均可以)。
第一題思路:
0-1 揹包 基礎問題,這裏揹包大小就是 n 萬美圓,物品就是 m 所學校,申請費 a 是物品的容量,可能性 b 是物品的價值。可是有一點須要注意的是計算可能性的時候,不能僅僅取最大值,正確的方法應該是
2、飯卡
電子科大本部食堂的飯卡有一種很詭異的設計,即在購買以前判斷餘額。若是購買一個商品以前,卡上的剩餘金額大於或等於5元,就必定能夠購買成功(即便購買後卡上餘額爲負),不然沒法購買(即便金額足夠)。因此你們都但願儘可能使卡上的餘額最少。某天,食堂中有n種菜出售,每種菜可購買一次。已知每種菜的價格以及卡上的餘額,問最少可以使卡上的餘額爲多少。
第二題思路:
揹包大小是卡里面的餘額,物品大小就是菜的價格。從 「每種菜可購買一次」 看出這實際上是一個 0-1 揹包 問題,這裏比較詭異的一點是,揹包容量能夠超,可是僅限於最後一個物品。因而這裏有一個思路上的變遷就是如何把這個詭異的揹包問題變成正常的揹包問題,若是能想到的話,其實不難,就是用 5 塊錢去買最貴的那道菜,而後 「卡上餘額 - 5」 做爲揹包的容量,其他的菜做爲物品,而後這道題的問法就是 「揹包能裝的最大容量是多少」,最後的答案是 「(5 - 最貴的菜) + (卡上總餘額 - 5 - 該揹包問題的解)」
你能夠看得出來得是,這裏花了很大得篇幅來將 0-1揹包 問題,的確,0-1 揹包 是最簡單的揹包問題,可是是其餘揹包問題的基礎,這種解題思路,以及這裏提到的一些寫代碼上面的優化技巧是能夠在其餘的揹包問題上覆用的。
題目
有 N 種物品和一個容量爲 V 的揹包,每種物品都有無限件可用。放入第 i 種物品 的費用是 C[i],價值是 W[i]。求解:將哪些物品裝入揹包,可以使這些物品的耗費的費用總和不超過揹包容量,且價值總和最大。求解這個最大價值
分析
和以前的 0-1 揹包 不一樣的是,徹底揹包 中的物品能夠任意取多少都行,若是對 0-1 揹包 理解的話,這裏在寫代碼的時候只須要作一點的改變,就是在決定取不取當前物品的時候,以前 0-1 揹包 是和以前不考慮當前物品的子問題結果作對比和更新,而 徹底揹包 相反,是和考慮當前物品的子問題作對比,代碼實現以下:
public static int completePack(int V, int[] C, int[] W) {
// 防止無效輸入
if (V == 0 || C.length != W.length) {
return 0;
}
int n = C.length;
// dp[i][j]: 對於下標爲 0~i 的物品,揹包容量爲 j 時的最大價值
int[][] dp = new int[n + 1][V + 1];
// 揹包空的狀況下,價值爲 0
dp[0][0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= V; ++j) {
// 不取該物品
dp[i][j] = dp[i - 1][j];
// 取該物品,可是是在考慮過或者取過該物品的基礎之上(dp[i][...])取
// 0-1揹包則是在尚未考慮過該物品的基礎之上(dp[i - 1][...])取
if ((j >= C[i - 1]) && (dp[i][j - C[i - 1]] + W[i - 1] > dp[i][j])) {
dp[i][j] = dp[i][j - C[i - 1]] + W[i - 1];
}
}
}
// 返回,對於全部物品(0~N),揹包容量爲 V 時的最大價值
return dp[n][V];
}
複製代碼
優化
空間和以前同樣,也是能夠優化到一維數組,可是這裏要注意的是,徹底揹包 考慮的值再也不是更新前的值了(dp[i - 1][...]),而是更新後的值(dp[i][...]),所以這時咱們的第二層 for 循環須要從前日後遍歷,保證當前考慮的值都是更新事後的值,代碼以下:
public static int completePackOpt(int V, int[] C, int[] W) {
if (V == 0 || C.length != W.length) {
return 0;
}
int n = C.length;
int[] dp = new int[V + 1];
for (int i = 0; i < n; ++i) {
for (int j = C[i]; j <= V; ++j) {
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
return dp[V];
}
複製代碼
對於 徹底揹包 問題,也會有不同的問法,可是和 0-1揹包 問題相似,能夠參照前面的內容,這裏不作贅述,這裏還有一道關於 徹底揹包 的題目能夠鞏固練習:
Piggy-bank
Before ACM can do anything, a budget must be prepared and the necessary financial support obtained. The main income for this action comes from Irreversibly Bound Money (IBM). The idea behind is simple. Whenever some ACM member has any small money, he takes all the coins and throws them into a piggy-bank. You know that this process is irreversible, the coins cannot be removed without breaking the pig. After a sufficiently long time, there should be enough cash in the piggy-bank to pay everything that needs to be paid. But there is a big problem with piggy-banks. It is not possible to determine how much money is inside. So we might break the pig into pieces only to find out that there is not enough money. Clearly, we want to avoid this unpleasant situation. The only possibility is to weigh the piggy-bank and try to guess how many coins are inside. Assume that we are able to determine the weight of the pig exactly and that we know the weights of all coins of a given currency. Then there is some minimum amount of money in the piggy-bank that we can guarantee. Your task is to find out this worst case and determine the minimum amount of cash inside the piggy-bank. We need your help. No more prematurely broken pigs!
解題思路:
最基本的 徹底揹包 問題,揹包大小是存錢罐的重量減去豬的重量,也就是裏面裝的錢幣的總重量,錢幣就是物品,每種錢幣能夠有無數個,可是注意這裏要求的是總價值的最小值。
題目
有 N 種物品和一個容量爲 V 的揹包。第 i 種物品最多有 M[i] 件可用,每件耗費的 空間是 C[i],價值是 W[i]。求解將哪些物品裝入揹包可以使這些物品的耗費的空間總和不超過揹包容量,且價值總和最大。求解該價值
分析
若是僅僅是要解決問題的話,其實很是簡單,咱們能夠把這個問題變成 0-1 揹包 問題去作,我這裏就直接套用 0-1 揹包 優化的版本,代碼以下:
public static int multiplePack1(int V, int[] C, int[] W, int[] M) {
int n = C.length;
int[] dp = new int[V + 1];
for (int i = 0; i < n; ++i) {
for (int k = 0; k < M[i]; ++k) {
// consider about zeroOnePack
for (int j = V; j >= C[i]; --j) {
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
}
return dp[V];
}
複製代碼
優化
上面代碼的時間複雜度是,就是咱們其實對每一個物品,無論相同與否,都遍歷了一遍。優化的話,能夠這樣思考,若是某個物品的總重量(C[i] * M[i] > V),那咱們其實就能夠考慮使用 徹底揹包 了,這樣的話,對於這個物品的計算時間上從以前的 V * M[i] 變成了 V,若是 M[i] 很大的話,時間上的節省仍是很可觀的;另一個優化就是在作 0-1 揹包 的時候,這裏有一個物品拆分的小優化,利用的是二進制的思想,假如 M[i] = 16,按照以前的思路,作 0-1 揹包 須要的時間是 16 * V,可是這裏的 16 能夠拆成,1 + 2 + 4 + 8 + 1,也就是說把這 16 個物品縮減成了 5 個物品,固然對應物品的價值等於分配的數量乘上單個物品的價值,這樣遍歷時間縮減爲 5 * V,其實就是將以前時間中的 M 變成了
。這兩個優化把時間複雜度降爲
,代碼以下:
public static int multiplePackOpt(int V, int[] C, int[] W, int[] M) {
int n = C.length;
int[] dp = new int[V + 1];
for (int i = 0; i < n; ++i) {
// go completePack
if (C[i] * M[i] >= V) {
for (int j = C[i]; j <= V; ++j) {
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
// go zeroOnePack
else {
// 1, 2, 2^2, 2^3,..., 2^(k-1), M[i] - 2^k + 1
int k = 1, tmp = M[i];
while (k < tmp) {
for (int j = V; j >= k * C[i]; --j) {
dp[j] = Math.max(dp[j], dp[j - k * C[i]] + k * W[i]);
}
tmp -= k;
k *= 2;
}
for (int j = V; j >= tmp * C[i]; --j) {
dp[j] = Math.max(dp[j], dp[j - tmp * C[i]] + tmp * W[i]);
}
}
}
return dp[V];
}
複製代碼
你能夠看到,對於 多重揹包 問題,難點是在優化上面,該問題綜合考慮了前面提到的 0-1 揹包 問題和 徹底揹包 問題,這樣的優化思路第一次看上去很陌生,可是寫的多了就不奇怪了,其實就是數學上面拆分數字的小技巧,同時這裏也有幾道題想和你分享,但願能幫你鞏固對揹包問題的認識:
1、Coins
Whuacmers use coins. They have coins of value A1,A2,A3...An Silverland dollar. One day Hibix opened purse and found there were some coins. He decided to buy a very nice watch in a nearby shop. He wanted to pay the exact price(without change) and he known the price would not more than m. But he didn't know the exact price of the watch. You are to write a program which reads n,m,A1,A2,A3...An and C1,C2,C3...Cn corresponding to the number of Tony's coins of value A1,A2,A3...An then calculate how many prices(form 1 to m) Tony can pay use these coins.
第一題思路:
基本的多重揹包問題,可是參考以前在 0-1 揹包 問題中講到的變形問法 「揹包能不能被裝滿」
2、Are You Busy
As having become a junior, xiaoA recognizes that there is not much time for her to AC problems, because there are some other things for her to do, which makes her nearly mad. What's more, her boss tells her that for some sets of duties, she must choose at least one job to do, but for some sets of things, she can only choose at most one to do, which is meaningless to the boss. And for others, she can do of her will. We just define the things that she can choose as "jobs". A job takes time , and gives xiaoA some points of happiness (which means that she is always willing to do the jobs). So can you choose the best sets of them to give her the maximum points of happiness and also to be a good junior(which means that she should follow the boss's advice)?
第二題思路:
算是多重揹包的變形題目,xiaoA 全部的時間就是揹包的容量,有三種物品,第一種是必須至少選一件,第二種是必須最多選一件,第三種是隨便怎麼選均可以,每一個物品也會有價值,價值就是成就感。這裏遍歷的順序很重要,先是遍歷必須至少選一件的狀況,而後是隨便選,最後是最多選一件的,要注意的是在後面兩次(隨便選,最多選一件)的遍歷中,要明確當前的揹包值是否選了至少選一件這一類中的物品。
以上即是我想要分享的全部揹包問題,其實揹包問題仍是比較常見的一類動態規劃問題,這樣子的分類或許對理解題目,尋找突破口頗有幫助,固然別忘了勤於練習,這裏的分享是基於揹包九講這一書的,最後會有下載連接,還有,若是以爲我有講的不到位的地方,歡迎指出,謝謝