一、“0-1背包”問題描述:
給定n中物品,物品i的重量是wi,其價值為vi,背包的容量為c.問應如何選擇裝入背包中的物品,使得裝入背包中的物品的總價值最大?
形式化描述:給定c>0,wi>0,vi>0,1≤i≤n,要求找一個n元0-1向量(x1,x2,...,xn),xi∈{0,1},1≤i≤n,使得∑wixi≤c,而且∑vixi達到最大。因此0-1背包問題是一個特殊的整形規划問題:
max ∑vixi
s.t ∑wixi≤c
xi∈{0,1},1≤i≤n
二、動態規划求解(兩種方法,順序或逆序法求解)
1.最優子結構性質
1.1 簡要描述
順序:將背包物品依次從1,2,...n編號,令i是容量為c共有n個物品的0-1背包問題最優解S的最高編號。則S'=S-{i}一定是容量為c-wi且有1,...,i-1項物品的最優解。如若不是,領S''為子問題最優解,則V(S''+{i})>V(S'+{i}),矛盾。這里V(S)=V(S')+vi.
逆序:令i是相應問題最優解的最低編號,類似可得。
1.2 數學形式化語言形式化的最優子結構
順序(從前往后):設(y1,y2,...,yn)是所給問題的一個最優解。則(y1,...,yn-1)是下面相應子問題的一個最優解:
max ∑vixi
s.t ∑wixi≤c
xi∈{0,1},1≤i≤n-1
如若不然,設(z1,...,zn-1)是上述子問題的一個最優解,而(y1,...,yn-1)不是它的最優解。由此可知,∑vizi>∑viyi,且∑vizi+wnyn≤c。因此
∑viyi+vnyn>∑viyi(前一個范圍是1~n-1,后一個是1~n)
∑vizi+wnyn≤c
這說明(z1,z2,...,yn)是一個所給問題的更優解,從而(y1,y2,...,yn)不是問題的所給問題的最優解,矛盾。
逆序:同樣的,是設(y2,...,yn)是相應子問題的最優解,其他類似。
2、階段決策過程描述(動態規划的標准形式,這里用順序法表示)
2.1 共有n個階段,階段k=1,2,...,n,每個階段便是需要作出一個決策的子問題部分。這里相當於可以將背包問題分解為n個子問題。
2.2 狀態與狀態變量xk:表示第k階段背包的容量
2.3 決策變量uk:表示k階段是否取物品k,即uk(xk)∈{0,1},是一個符號函數。
2.4 狀態轉移方程:xk+1 = xk+uk(xk)*xk
2.5 指標函數和最優指標函數,指標函數:衡量決策過程策略的優劣程度、數量指標。定義在全過程上的指標函數記為V1,k(或Vk,n,此時是逆序),有
V1,k=V1,k(x0,x1,u1,...,xk)
最優指標函數:當指標函數達到最優值時稱其為最優指標函數,記為fk(xk).它表示從第初始狀態x1起到狀態xk過程(或xk→xn,逆‘序),采取最優策略時得到的指標函數值,如下所示:
fk(xk)=opt V1,k
此處的最優指標函數值fk(xk):表示共有1,2,...,k個物品背包容量為xk的最優值如下:
為了便於編程實現,用m[i,j]表示fk(xk),表示背包容量為j,可選擇物品為1,2,...,i時0-1背包問題的最優解。則可得公式如下:
第1個公式表示初始值(邊界條件,令其為0)或者背包容量為0則結果也為0,第2個公式表示物品i的重量大於背包容量不能裝,第3個公式則是表示是否包含前物品i,前一個包含后一個不包含。
3.算法描述及其實現
3.1 自底向上的非遞歸算法如下:

3.2 源碼如下:

#include <iostream> #include <vector> using namespace std; //動態規划進行求解得出最優值m[n][c]中相應的值 void dynamic_01knap(int* w, float* v, int n, int c,vector<vector<float> >& m) { //最簡單的背包問題遞歸求解,w,v分別是重量、價值數組(從0開始) //n是物品個數,c是當前背包剩余的容量 //m是額外的表,存儲相應的最優值 int i = 0, j = 0; for (; j <= c; ++j) m[0][j] = 0;//邊界值 for (i = 1; i <= n; ++i) { m[i][0] = 0;//背包容量為空時 for (j = 1; j <= c; ++j){ if (w[i-1] <= j) //記住v,w是從0開始的 m[i][j] = m[i-1][j-w[i-1]]+v[i-1] > m[i-1][j] ? m[i-1][j-w[i-1]]+v[i-1] : m[i-1][j]; else m[i][j] = m[i-1][j]; cout << "m[" << i <<"][" << j << "]=" << m[i][j] << endl;//測試 } } } //找出相應的結果x[n] void traceback(int* w,float* v, int n,int c,vector<vector<float> >& m, vector<bool>& x) { //x是求解向量 int i = 0; for (i = n-1; i > 0; --i) { cout << "m[" << i <<"][" << c << "]=" << m[i][c] << //測試 ";m[" << i+1 <<"][" << c << "]=" << m[i+1][c] << endl; if (m[i][c] == m[i+1][c])//不取物品i+1,記住從0開始的 x[i] = false; else //取物品i+1,背包容量變小 { x[i] = true; c -= w[i]; } } x[0] = m[n][c] ? true : false; } int main() { int w[] = {10, 20, 30}, c = 50, i = 0, n = sizeof(w)/sizeof(w[0]); float v[] = {60, 100, 120}; vector<vector<float> > m(n+1, vector<float>(c+1)); vector<bool> x(n); dynamic_01knap(w,v,n,c,m); traceback(w,v,n,c,m,x); for (; i < n; ++i) cout << x[i] <<' '; cout << endl; }
注意,這里用的是順序法,即最優值為m[n][c],所以在輸出0-1數組(結果)的時候得從后往前輸;相反的,如果用逆序法,這最優值為m[1][c],輸出的時候則是從前往后輸。
3.3 自頂向下的遞歸算法-做備忘錄法(memoization)
memorized-0-1-knapsack(v, w, n, c) for i = 1 to n //初始化賦值為∞ for j = 1 to c m[i][j] = ∞; return lookup_max(v,w,n,c); lookup_max(v, w, i, j) if m[i][j] < ∞ return m[i][j] if i == 0 || j ==0 m[i][j] = 0; if w[i] <= j m[i][j] = lookup_max(v,w,i-1,j-w[i])+v[i] > lookup_max(v,w,i-1,j] ? lookup_max(v,w,i-1,j-w[i])+ v[i] : lookup_max(v,w,i-1,j]; else m[i][j] = lookup_max(v,w,i-1,j]; return m[i][j];
3.5 復雜度分析兩種方法的優劣比較
兩種算法的空間復雜度是一樣的,都是O(nc),時間復雜度數量級上也是一樣的,也為O(nc),但是因為背包問題有些子問題並不需要求解,所以第二種方法將第一種方法。
另外,上述方法有兩個明顯的缺點:
(1) 算法要求所給物品的重量是整數,而遞歸式中並無這要求;
(2) 當背包容量很大時,算法要求的計算時間較多。例如當c>2n時,需要Ω(n2n)。
針對這兩種情況,可以適當的改進,改進算法略;
二、回溯法求解
1. 回溯法的基本思想
回溯法是一種窮舉搜索方法,在明確問題的解空間(該問題解的所有情況,包括一些不滿足要求的解)后,將解空間組織成數或圖的形式,數的葉子結點的個數即是所有解的個數。
然后從開始結點(根結點)出發,以深度優先的方式搜索整個解空間。這個開始結點就成為一個活結點,同時也成為當前的擴展結點。在當前的擴展結點處,搜索向縱深方向移到一個新節點處。這個新節點就成為一個新的活結點,並成為當前擴展結點。
如果在當前的擴展結點處不能再向縱深方向移動,則當前的擴展結點就成為死結點。此時,應往回移動(回溯)至最近的一個活動結點處,並使這個貨結點成為當前的擴展結點。回溯法即以這種方式遞歸地在解空間中搜索,直到找到所求的解或解空間已無或結點為止。
在用回溯法搜索解空間樹時,通常采用兩種策略(加上剪枝函數)來避免無效搜索,提高搜索效率:
1)用約束函數在擴展結點處減去不滿足約束的子樹;
2)用限界函數減去不能得到最優解的子樹。
回溯法有兩種基本形式:遞歸回溯和迭代回溯(這兩種方法可以互相轉換,因為一般回溯法的遞歸回溯是尾遞歸,所以迭代回溯一般也不用用棧)。
用回溯法解題的一個顯著特征是問題的解空間是在搜索過程中動態產生的。在任何時刻,算法之保存從根結點到當前擴展結點的路徑。如果解空間中從根結點到葉子結點的最長路徑長度為h(n),則回溯法所需的計算空間通常為O(h(n)),另外回溯法的時間復雜度為葉子結點的個數。
解空間一般有兩種形式:
1)子集樹:所給問題是從n個元素的集合s中找出滿足某種性質的子集時,相應的解空間樹。相當於求結合s的冪集。這類子集樹通常有2n個葉子結點,其結點總個數為2n+1-1,時間復雜度為Ω(2n);
2)排列數:同理,所給問題是確定n個元素滿足某種性質的排列時的相應的解空間。遍歷排列數通常需要Ω(n!).
2. 背包問題的回溯法求解
2.1 基本思路
顯然可以用子集樹表示其解空間,解的所有可能情況。用可行性約束減去不滿足約束的子樹,用上界函數剪去不能得到最優解的子樹。
2.2 源程序如下:

#include <iostream> #include <vector> #include <algorithm> using namespace std; class Goods{ public: int id; float v;//商品價值 float w; //商品重量 float p; //商品單價 bool operator <= (Goods G) const { return (p >= G.p); } }; class Knap{ friend float knapsack(vector<Goods> Gs, float c,int n);//返回最優裝載重量 private: float bound(int i); //計算上界 void backtrack(int i); int n; //物品數 vector<Goods> goods; //物品數組 float c,//背包容量 cw, //當前重量 cp, //當前價值 bestp; //當前最優價值 bool *x, //當前解 *bestx; //當前最優解 }; float Knap::bound(int i) { float cleft = c - cw;//剩余背包容量 float b = cp; //假設物品已經按單位重量價值遞減排序裝入物品 while (i<n && goods[i-1].w <= cleft) { cleft -= goods[i-1].w; b += goods[i-1].v; ++i; } //將背包裝滿 if (i <= n) b += goods[i-1].p*cleft; return b; } void Knap::backtrack(int i) {//搜索第i層的結點 if (i > n) //到達葉子結點 { if (cp >bestp) { bestp = cp; for (int j = 0; j < n; ++j) bestx[j] = x[j]; } return; } if(cw + goods[i-1].w <= c) //x[i] = 1搜索左子樹 { x[i-1] = true; cw += goods[i-1].w; cp += goods[i-1].v; backtrack(i+1); //返回至該結點 cw -= goods[i-1].w; cp -= goods[i-1].v; } if (bound(i+1) > bestp) //x[i] = 0搜索右子樹 { x[i-1] = false; backtrack(i+1); } } bool Upgreater ( Goods g1, Goods g2 ) { return (g1<=g2); } float knapsack(vector<Goods> Gs, float c,int n) //返回最優裝載重量 { //為Knap::backtrck初始化 int i = 0; float W = 0, P = 0; Knap K; K.goods = Gs; for (i = 0; i < n; ++i) { P += Gs[i].p; W += Gs[i].w; } if (W <= c) //裝入所有物品 return P; sort(K.goods.begin(),K.goods.end(),Upgreater); K.cp = 0; K.cw = 0; K.c = c; K.n = n; K.bestp = 0; K.x = new bool[n]; K.bestx = new bool[n]; //回溯搜索 K.backtrack(1); cout << "裝載情況為:" << endl; for (i = 0; i < n; ++i) cout << "物品id=" << K.goods[i].id <<":" << K.bestx[i] << endl; delete[] K.x; delete[] K.bestx; return K.bestp; } int main() { int n = 4, i =0; float c = 7, bestp, w[] = {3, 5, 2, 1}, v[] = {9, 10, 7, 4}; vector<Goods> Gs(n); for (i = 0; i < n; ++i) { Gs[i].id = i+1; Gs[i].w = w[i]; Gs[i].v = v[i]; Gs[i].p = v[i]/w[i]; } bestp = knapsack(Gs,c,n); cout << "該背包的最優裝載價值為:" << bestp << endl; return 0; }