遞迴、回溯-0-1揹包問題
0-1揹包問題是子集選取問題。一般情況下,0-1揹包問題是NP難的,0-1揹包問題的解空間可用子集樹表示。解0-1揹包問題的回溯法與解裝載問題的回溯法十分相似。
在搜尋解空間樹時,只要其左兒子結點是一個可行結點,搜尋就進入其左子樹。當右子樹有可能包含最優解時才進入右子樹搜尋,否則將右子樹剪去。設r是當前剩餘物品價值總和:cp是當前價值;bestp是當前最優價值。當cp+r<=bestp時,可剪去右子樹。計算中解的上界的更好方法是剩餘物品依其單位重量價值排序,然後依次裝入物品,直到裝不下時,再裝入該物品的一部分而裝滿揹包。由此得到的價值是右子樹解的上界。
例如,對於0-1揹包問題的一個例項,n=4,c=7,p=[9,10,7,4],w=[3,5,2,1].這個物品的單位重量價值分別為[3,2,3,5,4],以物品單位重量價值的遞減序裝入物品,先裝入物品4,然後裝入物品3和1,。裝入這3個物品後,剩餘的揹包容量為1,只能裝入0.2的物品2.由此得到一個解為x = [1,0.2,1,1],其相應的價值為22.儘管這不是一個可行解,但可以證明其價值是最優解的上界。因此,對於這個例項,最優值不超過22。
輸入:物品的數目n,揹包的容量c。各個物品的重量wi,各個物品的價值vi。
輸出:裝入揹包的最大價值。
執行結果:
為了便於計算上界,可先將物品依其單位重量價值從大到小排序,此後只要按順序考察各物品即可。在實現時,由Bound計算當前結點處的上界。類Knap的資料成員記錄解空間樹中的結點資訊,以減少引數傳遞及遞迴呼叫所需的棧空間。在解空間樹的當前擴充套件結點處。僅當要進入右子樹時才計算上界Bound,以判斷是否可將右子樹剪去。進入左子樹時不需計算上界,因為其上界與其父結點的上界相同。
template <class Typew, class Typep> class Knap { template <class Tw, class Tp> friend Tp Knapsack(Tw *, Tp *, Tw, int); private: Typep Bound(int i); void BackTrace(int i); int n; //裝包物品重量 Typew c, //揹包容量 *w, //物品重量陣列 cw; //當前重量,從根到當前結點所形成的部分解的裝包物品重量 Typep cp, //當前價值,從根到當前結點所形成的部分解的裝包物品價值 *p, //物品價值陣列 bestp; //當前最優價值,已搜尋過得部分解空間樹的最優裝包價值 }; //計算以當前結點為根的子樹的價值上界 //計算上界的方法是將剩餘物品依其單位重量價值排序,然後依次裝入物品,直至裝不下時,再裝入該物品的一部分而裝滿揹包。 template <class Typew, class Typep> Typep Knap<Typew, Typep>::Bound(int i) { Typew cleft; //剩餘容量 Typep b; cleft = c - cw; b = cp; while(i <= n && w[i] <= cleft) //以物品單位重量價值遞減序裝入物品 { b += p[i]; cleft -= w[i]; i++; } if(i <= n) //裝滿揹包,剩餘的容量不足一個,裝一部分 b += p[i]*cleft / w[i]; return b; } //對解空間樹回溯搜尋,求得最大裝包價值 template <class Typew, class Typep> void Knap<Typew, Typep>::BackTrace(int i) { if(i > n) //到達葉節點 { bestp = cp; return; } if(cw + w[i] <= c) //滿足約束函式,進入左子樹 { cw += w[i]; cp += p[i]; BackTrace(i+1); cw -= w[i]; //回溯還原 cp -= p[i]; } if(Bound(i+1) > bestp) //滿足限界函式,進入右子樹 BackTrace(i+1); }
//物品 class Object { template <class Tw, class Tp> friend Tp Knapsack(Tw *, Tp *, Tw, int); public: bool operator < (const Object &a) const { return d > a.d; } private: int ID; //物品標號 float d; //單位重量價值 };
template <class Typew, class Typep>
Typep Knapsack(Typew *w, Typep *p, Typew c, int n)
{
Typew W;
Typep P;
Object *Q;
int i;
//初始化
W = 0;
P = 0;
Q = new Object[n];
for(i = 1; i <= n; i++)
{
Q[i-1].ID = i;
Q[i-1].d = 1.0*p[i] / w[i];
W += w[i];
P += p[i];
}
if(W <= c) //能夠裝入所有物品
return P;
sort(Q, Q+n); //將n個物品依單位重量價值排序
Knap<Typew, Typep> K;
K.c = c;
K.n = n;
K.bestp = 0;
K.cw = 0;
K.cp = 0;
K.p = new Typep[n+1];
K.w = new Typew[n+1];
for(i = 1; i <= n; i++)
{
K.w[i] = w[Q[i-1].ID];
K.p[i] = p[Q[i-1].ID];
}
K.BackTrace(1);
delete []Q;
delete []K.p;
delete []K.w;
return K.bestp; //返回最大裝包價值
}
2、演算法效率
計算上界需要O(n)時間,在最壞情況下有個右兒子結點需要計算上界,故解0-1揹包問題的回溯演算法Backtrack所需的計算時間為.