01背包問題 (問題描述):
給定 n 件物品,物品的重量為 w[i],物品的價值為 c[i]。現挑選物品放入背包中,假定背包能承受的最大重量為 V,問應該如何選擇裝入背包中的物品,使得裝入背包中物品的總價值最大?
一個有趣的例子:
假設你是一個小偷,背着一個可裝下4磅東西的背包,你可以偷竊的物品如下:
為了讓偷竊的商品價值最高,你該選擇哪些商品?
物品名 | 重(磅) | 價值(美元) |
吉他 | 1 | 1500 |
音響 | 4 | 3000 |
筆記本 | 3 | 2000 |
直接上手嘗試組合在這種數量比較少的情況還可行,直接可得出偷 吉他和筆記本,總重4磅,價值3500美元。然而要是換一個問題,比如最近 B 站首屆bilibili 1024 安全挑戰賽的一道題如下:
3.期末考試結束了,老師決定帶學生們去卷餅店吃烤鴨餅。老師看到大餅和鴨子,搞了一個活動:每人可以拿走一張餅,誰卷到的食物美味程度總和最高,誰就能獲得稱號:卷王之王!Vita很想得到“卷王之王”稱號,他的大餅可以裝下大小總和不超過500的食物,現在有7塊鴨肉和6根黃瓜,每份食物都有它的大小和美味程度。 每塊鴨肉的大小:85、86、73、66、114、51、99 每塊鴨肉的美味程度:71、103、44、87、112、78、36 每根黃瓜的大小:35、44、27、41、65、38 每塊黃瓜的美味程度:41、46、13、74、71、27 老師要求大餅里至少有一塊鴨肉和一根黃瓜。請問,Vita卷到的食物美味程度總和最大是多少?(本題由UP主@小學生Vita君提供)
A. 593 B.612 C.496 D. 584
直接嘗試組合恐怕不是一件容易的事,我們需要一個更便捷的策略來輔助我們解決這樣的問題。
這里要嘗試解釋的,就是耳熟能詳的動態規划,百度百科里這樣解釋動態規划的基本思想:
動態規划算法通常用於求解具有某種最優性質的問題。在這類問題中,可能會有許多可行解。每一個解都對應於一個值,我們希望找到具有最優值的解。動態規划算法與分治法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然后從這些子問題的解得到原問題的解。與分治法不同的是,適合於用動態規划求解的問題,經分解得到子問題往往不是互相獨立的。若用分治法來解這類問題,則分解得到的子問題數目太多,有些子問題被重復計算了很多次。如果我們能夠保存已解決的子問題的答案,而在需要時再找出已求得的答案,這樣就可以避免大量的重復計算,節省時間。我們可以用一個表來記錄所有已解的子問題的答案。不管該子問題以后是否被用到,只要它被計算過,就將其結果填入表中。這就是動態規划法的基本思路。
至於為啥叫 “動態”,百度百科里這樣解釋其概念:
在現實生活中,有一類活動的過程,由於它的特殊性,可將過程分成若干個互相聯系的階段,在它的每一階段都需要作出決策,從而使整個過程達到最好的活動效果。因此各個階段決策的選取不能任意確定,它依賴於當前面臨的狀態,又影響以后的發展。當各個階段決策確定后,就組成一個決策序列,因而也就確定了整個過程的一條活動路線.這種把一個問題看作是一個前后關聯具有鏈狀結構的多階段過程就稱為多階段決策過程,這種問題稱為多階段決策問題。在多階段決策問題中,各個階段采取的決策,一般來說是與時間有關的,決策依賴於當前狀態,又隨即引起狀態的轉移,一個決策序列就是在變化的狀態中產生出來的,故有“動態”的含義,稱這種解決多階段決策最優化的過程為動態規划方法
這些定義雖然很准確,但是確實有一定程度的抽象,如果在理解的時候能夠把這個理解具體化,那就好理解一些。所以接下來還是回到上面那個小偷的例子,一步一步構建出動態規划的核心:狀態表格。
關於構建狀態表格
網格的各行表示商品,各列代表不同容量(1~4磅)的背包
1. 在填第一行時,我們可選的商品只有吉他;在填第二行時,我們可選的商品則可以有吉他和音響;同理,在填第三行時,可選吉他,音響,筆記本電腦
2. 最后一行的最后一列,對應於這里的第三行第四列所填寫的數字,表示在背包容器為 4 的情況下,從吉他,音響,筆記本選出的最佳組合的價值。
3. 用動態規划解決 01背包問題 的巧妙之處類似於遞歸的優雅簡潔,你要做的就是將一個看似復雜的問題,分解為自相似的子問題
n! = n *(n-1)! //求一個數 n 的階乘,可以將其分解為: 使用 n 乘以 n-1 的階乘
這里背包問題的分解方式是:求能裝4磅背包裝什么價值最高,分解為假設我先拿下一個筆記本電腦(3磅),剩下的容量(1磅) 能夠裝什么價值最高(子問題)
這里構建的表格,每一個格子就是一個問題(求能裝某個磅數重量的背包裝什么價值最高),而該問題的子問題答案,總能在前面已經構建的表格中找到答案
越往前問題就越小,最前面的一行自然是最最簡單的,所以,這里構建表格的方式是:
4. 我們將一行一行從上往下填
開始構建表格
第一行:
這一行,我們可選的商品只有吉他
第一個單元格表示背包的的容量為1磅。吉他的重量也是1磅,這意味着它能裝入背包!因此這個單元格包含吉他,價值為1500美元。
與這個單元格一樣,每個單元格都將包含當前可裝入背包的所有商品。
來看下一個單元格。這個單元格表示背包容量為2磅,完全能夠裝下吉他!
這行的其他單元格也一樣。別忘了,這是第一行,只有吉他可供你選擇,換而言之,你假裝現在還沒發偷竊其他兩件商品
此時你很可能心存疑惑:原來的問題說的額是4磅的背包,我們為何要考慮容量為1磅、2磅等得背包呢?前面說過,動態規划從小問題着手,逐步解決大問題。這里解決的子問題將幫助你解決大問題。
別忘了,你要做的是讓背包中商品的價值最大。這行表示的是當前的最大價值。它指出,如果你有一個容量4磅的背包,可在其中裝入的商品的最大價值為1500美元。
你知道這不是最終解。隨着算法往下執行,你將逐步修改最大價值。
第二行:
你現在處於第二行,可以偷竊的商品有吉他和音響。
我們先來看第一個單元格,它表示容量為1磅的背包。在此之前,可裝入1磅背包的商品最大價值為1500美元。
該不該偷音響呢?
背包的容量為1磅,顯然不能裝下音響。由於容量為1磅的背包裝不下音響,因此最大價值依然是1500美元。
接下來的兩個單元格的情況與此相同。在這些單元格中,背包的容量分別為2磅和3磅,而以前的最大價值為1500美元。由於這些背包裝不下音響,因此最大的價值保持不變。
背包容量為4磅呢?終於能夠裝下音響了!原來最大價值為1500美元,但如果在背包中裝入音響而不是吉他,價值將為3000美元!因此還是偷音響吧。
你更新了最大價值。如果背包的容量為4磅,就能裝入價值至少3000美元的商品。在這個網格中,你逐步地更新最大價值。
第三行:
下面以同樣的方式處理筆記本電腦。筆記本電腦重3磅,沒法將其裝入1磅或者2磅的背包,因此前兩個單元格的最大價值仍然是1500美元。
對於容量為3磅的背包,原來的最大價值為1500美元,但現在你可以選擇偷竊價值2000美元的筆記本電腦而不是吉他,這樣新的最大價值將為2000美元。
對於容量為4磅的背包,情況很有趣。這是非常重要的部分。當前的最大價值為3000美元,你可不偷音響,而偷筆記本電腦,但它只值2000美元。
價值沒有原來高,但是等一等,筆記本電腦的重量只有3磅,背包還有1磅的重量沒用!
在1磅的容量中,可裝入的商品的最大價值是多少呢?你之前計算過。
根據之前計算的最大價值可知,在1磅的容量中可裝入吉他,價值1500美元。因此,你需要做如下的比較:
答案如下:將吉他和筆記本電腦裝入背包時價值更高,為3500美元。
你可能認為,計算最后一個單元格的價值時,我使用了不同的公式。那是因為填充之前的單元格時,我故意避開了一些復雜的因素。其實,計算每個單元格的價值時,使用的公式都相同。這個公式如下。
同樣的容量,在原來考慮的商品之上,多考慮一個商品,只有兩種可能:“要么考慮先加入當前商品,要么不考慮直接還是用原先的組合”。哪種更好,選擇哪個。
這個,就是動態規划求解背包問題的核心秘訣。
第四行:
知道這個秘密后,我們再來添加一個商品,鞏固一下這個計算方式:
現在假設還有第四件商品可偷——一個iPhone
此時需要重新執行前面所做的計算嗎?不需要。別忘了,動態規划逐步計算最大價值。到目前為止,計算出的最大價值如下:
這意味着背包容量為4磅時,你最多可偷價值3500美元的商品。但這是以前的情況,下面再添加表示iPhone的行。
我們還是從第一個單元格開始。
“要么考慮先加入當前商品,要么不考慮直接還是用原先的組合”
iPhone可裝入容量為1磅的背包,放下之后填滿沒有剩余空間。之前的最大價值為1500美元,但iPhone價值2000美元,因此該偷iPhone而不是吉他。
在下一個單元格中,
“要么考慮先加入當前商品,要么不考慮直接還是用原先的組合”
2磅容器,考慮 1磅的 iPhone,剩下 1 磅空間,1500美元的吉他,共可裝入iPhone和吉他,比 剛剛不用 iPhone 好。
對於第三個單元格,
“要么考慮先加入當前商品,要么不考慮直接還是用原先的組合”
3磅容器,考慮 1磅的 iPhone,剩下 2 磅空間,根據前面的評估,也是剩下1500美元的吉他,也是裝入iPhone和吉他 比 剛剛不用 iPhone 好。
對於最后一個單元格,情況比較有趣。
同樣,“要么考慮先加入當前商品,要么不考慮直接還是用原先的組合”
當前的最大價值為3500美元,但你可以偷iPhone,這將余下3磅的容量。
3磅容量的最大價值為2000美元!再加上iPhone價值2000美元,總價值為4000美元。新的最大價值誕生了!
最終的網格如下。
代碼實現參考:

#include <vector> #include <iostream> //解決方案 class solution { public: solution(int value):totalValue(value) {} int totalValue; //選擇的物品的總價值 std::vector<size_t> items; //選擇的物品的項 int containerValue; //容器容量 }; //構建網格 solution buildNet(const std::vector<size_t>& w, const std::vector<int>& v, size_t total) { size_t row = w.size(); //可選擇的物體數量 size_t column = total; //總容量 std::vector<std::vector<solution>> net; net = std::vector<std::vector<solution>>(row+1, std::vector<solution>(column+1, 0)); //初始化多第一行和第一列,便於通用公式 for (size_t r = 1; r <= row; ++r) { for (size_t c = 1; c <= column; ++c) { size_t weightCurrent = w[r - 1]; //當前物品重 int valueCurrent = v[r - 1]; //當前物品價值 if (weightCurrent <= c) //如果單獨放得下 { int valueIncludeCurrent = valueCurrent + net[r - 1][c - weightCurrent].totalValue; if (valueIncludeCurrent > net[r - 1][c].totalValue) //加入當前物品價值更高,則更新方案 { net[r][c] = valueIncludeCurrent; net[r][c].items = net[r - 1][c - weightCurrent].items; //得到之前的序列 net[r][c].items.push_back(r); //添加自己到序列后 } else net[r][c] = net[r - 1][c]; } else net[r][c] = net[r - 1][c]; } } net[row][column].containerValue = total; return net[row][column]; } //打印選擇的最佳方案 void printVector(const std::vector<size_t>& w, const std::vector<int>& v,const solution & s) { std::cout << "Input: "; for (size_t i = 0; i < w.size(); ++i) { std::cout << w[i] << " (" << v[i] << ");" ; } std::cout << "Container: " << s.containerValue << std::endl; const std::vector<size_t>& items = s.items; int totalV = s.totalValue; size_t totalW = 0; size_t totalV2 = 0; for (auto r : items) { size_t w0 = w[r-1]; int v0 = v[r-1]; std::cout << w0 << " (" << v0 << ");" << std::endl; totalW += w0; totalV2 += v0; } std::cout << "Total: " << totalW << " (" << totalV << " -> check:" << totalV2 << ")"; std::cout << std::endl << std::endl; } int main() { std::vector<size_t> w = {1,4,3,1}; std::vector<int> v = { 1500,3000,2000,2000}; solution maxValue = buildNet(w, v, 4); printVector(w, v, maxValue); std::vector<size_t> w2 = { 85, 86, 73, 66, 114, 51, 99 }; std::vector<int> v2 = { 71,103,44, 87, 112, 78, 36 }; solution duck = buildNet(w2, v2, 500); printVector(w2, v2, duck); std::vector<size_t> w3 = { 35, 44, 27, 41, 65, 38 }; std::vector<int> v3 = { 41, 46, 13, 74, 71, 27 }; solution cucumber = buildNet(w3, v3, 500); printVector(w3, v3, cucumber); std::vector<size_t> w4 = w2; w4.insert(w4.end(), w3.begin(), w3.end()); std::vector<int> v4 = v2; v4.insert(v4.end(), v3.begin(), v3.end()); solution duckCucumber = buildNet(w4, v4, 500); printVector(w4, v4, duckCucumber); return 0; }
參考1:(0-1背包問題 - 簡書 by 我沒有三顆心臟):https://www.jianshu.com/p/a66d5ce49df5
參考2:(動態規划之01背包問題 - 簡書 by kkbill):https://www.cnblogs.com/kkbill/p/12081172.html