分支限界法-0-1揹包問題
分支限界法類似於回溯法,也是在問題的解空間上搜索問題解的演算法。一般情況下,分支限界法與回溯法的求解目標不同。回溯法的求解目標是找出解空間中滿足約束條件的所有解,而分支限界法的求解目標則是找出滿足約束條件的一個解,或是在滿足約束條件的解中找出使某一目標函式值達到極大或極小的解,即在某種意義下的最優解。
由於求解目標不同,導致分支限界法與回溯法對解空間的搜尋也不相同。回溯法以深度優先的方式搜尋解空間,而分支限界法則以廣度優先或以最小耗費優先的方式搜尋解空間。
分支限界法的搜尋策略是:在擴充套件結點處,先生成其所有的兒子結點(分支),然後再從當前的活結點表中選擇下一個擴充套件結點。為了有效地選擇下一擴充套件結點,加速搜尋的程序,在每一活結點處,計算一個函式值(限界),並根據函式值,從前活結點表中選擇一個最有利的結點作為擴充套件結點,使搜尋朝著解空間上有最優解的分支推進,以便儘快得找出一個最優解。這種方法稱為分支限界法。人們已經用分支限界法解決了大量離散最優化的問題。
分支限界法即基本思想:
分支限界法常以廣度優先或以最小耗費(最大效益)優先的方式搜尋問題的解空間樹。問題的解空間樹是表示問題解空間的一顆有序樹,常見的有子集樹和排列樹。在搜尋問題的解空間樹時,分支限界法與回溯法的主要區別在於它們對當前擴充套件結點所採用的擴充套件方式不同。在分支限界法中,每一個活結點只有一次機會成為擴充套件結點。活結點一旦成為擴充套件結點,就一次性產生其所有兒子結點。在這些兒子結點中,導致不可行解或導致非最優解的兒子結點被捨棄,其餘兒子結點被加入活結點表中,此後,從活結點表中取下一結點成為當前擴充套件結點,並重覆上述結點擴充套件過程。這個過程一直持續到找到所需的解或活結點表為空時為止。
從活結點表中選擇下一擴充套件結點的不同方式導致不同的分支限界法。最常見的有一下兩種方式。
(1)佇列式(FIFO)分支限界法
佇列式分支限界法將活結點表組織成一個佇列,並按佇列的先進先出原則選擇下一個結點為當前擴充套件結點。
(2)優先佇列式分支限界法
優先佇列式的分支限界法將活結點表組織成一個優先佇列,並按優先佇列中規定的結點優先順序選擇優先順序最高的下一個結點成為當前擴充套件結點。優先佇列中規定的結點優先順序常用一個與該結點相關的數值p來表示。結點優先順序的高低與p值的大小相關。
看一下熟悉的0-1揹包問題怎麼用分支限界法來解決。
輸入:物品的數目n,揹包的容量c。各個物品的重量wi,各個物品的價值vi。
輸出:裝入揹包的最大價值、物品i是否裝入揹包,是的話輸出1,反之輸出0
執行結果:
在解0-1揹包問題的優先佇列式分支限界法中,活結點優先佇列中結點元素N的優先順序由該結點的上界函式Bound計算處的值uprofit給出。該上界函式在解0-1揹包問題的回溯法中討論過,子集樹中以結點N為根的子樹中任一結點的價值不超過N.profit,可用一個最大堆來實現活結點優先佇列。堆中元素型別為HeapNode,其私有成員有uprofit,profit,weight和level。對於任意活結點N,N.weight是結點N所相應的重量;N.profit是N所相應的價值;N.uprofit是結點N的價值上界,最大堆以這個值作為優先順序。子集空間樹中結點型別為bbnode。
//物品
class Object
{
template <class Typew, class Typep>
friend Typep Knapsack(Typep *, Typew *, Typew, int, int *);
public:
bool operator < (const Object &Q) const
{
return d > Q.d;
}
private:
int ID; //物品標號
float d; //單位重量價值
};
//類的宣告
template <class Typew, class Typep>
class Knap;
class bbnode
{
template <class Typew, class Typep>
friend class Knap;
private:
bbnode *parent; //指向父結點的指標
bool LChild; //左兒子結點標誌
};
//堆結點
template <class Typew, class Typep>
class HeapNode
{
template <class Tw, class Tp>
friend class Knap;
public:
bool operator < (const HeapNode &H) const
{
return uprofit < H.uprofit;
}
private:
Typew weight; //結點所對應的重量
Typep profit, //結點所對應的價值
uprofit; //結點的價值上界
bbnode *ptr; //指向活結點在子集樹中相應結點的指標
int level; //活結點在子集樹中所處的層序號
};
演算法中用到的類Knap與解0-1揹包問題的回溯法中用到的類Kanp十分相似。它們的區別是新的類中沒有成員變數bestp,而增加了新的成員bestx。bestx[i]=1當且僅當最優解含有物品i。
template <class Typew, class Typep>
class Knap
{
template <class Tw, class Tp>
friend Tp Knapsack(Tp *, Tw *, Tw, int, int *);
public:
Typep MaxKnapsack();
private:
Typep Bound(int i);
void AddLiveNode(Typep up, Typep cp, Typew cw, bool ch, int lev);
priority_queue<HeapNode<Typew,Typep> > pq; //優先佇列
int n; //物品總數
Typew cw, //當前裝包重量
*w, //物品重量陣列
c; //揹包容量
Typep cp, //當前裝包價值
*p; //物品價值陣列
int *bestx; //最優解
bbnode *E; //指向擴充套件結點的指標
};
上界函式Bound計算結點所相應價值的上界。
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)
{
cleft -= w[i];
b += p[i];
i++;
}
//裝填剩餘容量裝滿揹包
if(i <= n)
b += 1.0*p[i]/w[i] * cleft;
return b;
}
函式AddLiveNode將一個新的活結點插入到子集樹和優先佇列中。
template <class Typew, class Typep>
void Knap<Typew, Typep>::AddLiveNode(Typep up, Typep cp, Typew cw, bool ch, int lev)
{
//將一個新的活結點插入到子集樹和最大堆H中
bbnode *b;
HeapNode<Typew, Typep> N;
b = new bbnode;
b->parent = E;
b->LChild = ch;
N.weight = cw;
N.uprofit = up;
N.profit = cp;
N.level = lev;
N.ptr = b;
pq.push(N);
}
演算法MaxKanpsack實施對子集樹的優先佇列式分支限界搜尋。其中假定各物品依其單位重量價值從大到小排序。相應的排序過程可在演算法的預處理部分完成。
演算法中E是當前擴充套件結點;cw是該結點所相應的重量;cp是相應的價值;up是價值上界。演算法的while迴圈不斷擴充套件結點,直到子集樹的葉結點稱為擴充套件結點時為止。此時優先佇列中所有活結點的價值上界均不超過該葉結點的價值。因此該葉結點相應的解為問題的最優值。
在while迴圈內部,演算法首先檢查當前擴充套件結點的左兒子結點的可行性。如果該左兒子結點是可行結點,則將它加入到子集樹和活結點優先佇列中。當前擴充套件結點的右兒子結點一定是可行結點,僅當右兒子結點滿足上界約束時才將它加入子集樹和活結點優先佇列。
template <class Typew, class Typep>
Typep Knap<Typew, Typep>::MaxKnapsack()
{
//返回最大價值,bestx返回最優解
int i;
Typep bestp, up;
Typew wt;
HeapNode<Typew, Typep> N;
pq = priority_queue<HeapNode<Typew,Typep> >();
i = 1;
E = 0;
bestp = 0;
cw = 0;
cp = 0;
bestx = new int[n+1]; //為bestx分配儲存空間
up = Bound(1); //價值上界
while(i != n+1) //非葉結點
{
//檢查當前擴充套件結點的左兒子結點
wt = cw + w[i];
if(wt <= c) //左兒子結點為可行結點
{
if(cp + p[i] > bestp) //更新bestp
bestp = cp + p[i];
AddLiveNode(up, cp + p[i], cw + w[i], true, i+1);
}
//檢查當前擴充套件結點的右兒子
up = Bound(i+1);
if(up >= bestp) //右兒子可能含最優解
AddLiveNode(up, cp, cw, false, i+1);
//取下一擴充套件結點
N = pq.top();
pq.pop();
E = N.ptr;
cw = N.weight;
cp = N.profit;
up = N.uprofit;
i = N.level;
}
//構造當前最優解
for(i = n; i > 0; i--)
{
bestx[i] = E -> LChild;
E = E -> parent;
}
return cp;
}
下面的Knapsack完成對輸入資料的預處理。其主要任務是將各物品依其單位重量價值從大到小排好序。然後呼叫MaxKnapsack完成對子集樹的優先佇列式分支限界搜尋。
template <class Typew, class Typep>
Typep Knapsack(Typep *p, Typew *w, Typew c, int n, int *bestx)
{
//返回最大價值,bestx返回最優解
//初始化
Object *Q;
Typew W;
Typep P, bestp;
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); //依單位重量價值排序
Knap<Typew, Typep> K; //建立類Knap的資料成員
K.p = new Typep[n+1];
K.w = new Typew[n+1];
for(i = 1; i <= n; i++) //初始化
{
K.p[i] = p[Q[i-1].ID];
K.w[i] = w[Q[i-1].ID];
}
K.n = n;
K.c = c;
K.cp = 0;
K.cw = 0;
bestp = K.MaxKnapsack(); //呼叫MaxKnapsack求問題的最優解
for(i = 1; i <= n; i++)
bestx[Q[i-1].ID] = K.bestx[i];
delete []Q;
delete []K.p;
delete []K.w;
delete []K.bestx;
return bestp;
}