1.分枝—限界法的基本原理 分枝—限界算法類似於回溯法,也是一種在問題的解空間樹上搜索問題解的算法。但兩者求解方法有兩點不同:第一,回溯法只通過約束條件剪去非可行解,而分枝—限界法不僅通過約束條件,而且通過目標函數的限界來減少無效搜索,也就是剪掉了某些不包含最優解的可行解;第二,在解空間樹上,回溯法以深度優先搜索,而分枝—限界法則以廣度優先或最小耗費優先的方式搜索。分枝—限界的搜索策略是,在擴展節點處,首先生成其所有的兒子結點(分支),然后再從當前的活結點表中選擇下一個擴展結點。為了有效地選擇下一擴展結點,以加速搜索進程,在每一活結點處,計算一個函數值(限界),並根據這些已計算出的函數值從當前活結點表中選擇一個最有利的結點做為擴展,使搜索朝着解空間樹上最優解的分支推進,以便盡快找出一個最優解。 分枝—限界法常以廣度優先或以最小耗費優先的方式搜索問題的解空間樹(問題的解空間樹是表示問題皆空間的一顆有序樹,常見的有子集樹和排序樹)。在搜索問題的解空間樹時,分枝—限界法的每一個活結點只有一次機會成為擴展結點。活結點一旦成為擴展結點,就一次性產生其所有兒子結點。在這些兒子結點中,那些導致不可行解或非最優解的兒子結點將被舍棄,其余兒子結點被加入活結點表中。此后,從活結點表取出下一結點成為當前擴展結點,並重復上述擴展過程,直到找到最優解或活結點表為空時停止。 2. 0-1背包問題的分枝—限界算法的數據結構 template<class Typew,class Typep> class HeapNode { friend Knap<Typew,Typep>; public: operator Typep() const { return uprofit; } private: Typep uprofit, //節點的價值上界 profit; //節點所相應的價值 Typew weight; //節點所相應的重量 int level; //活節點在子集樹中所處的層序號 bbnode *ptr; //指向活節點在子集中相應節點的指針 }; 3. 限界函數: Typep Knap<Typew,Typep>::Bound(int i)//計算節點所相應價值的上界 { Typew cleft = c-cw; //剩余容量高 Typep b = cp; //價值上界 //以物品單位重量價值遞減序裝填剩余容量 while(i<=n && w[i]<=cleft) { cleft -= w[i]; b += p[i]; i++; } 4.算法具體實現主要代碼如下: //0-1背包問題 分支限界法求解 #include "stdafx.h" #include "MaxHeap.h" #include <iostream> using namespace std; class Object { template<class Typew,class Typep> friend Typep Knapsack(Typep p[],Typew w[],Typew c,int n, int bestx[]); public: int operator <= (Object a) const { return d>=a.d; } private: int ID; float d;//單位重量價值 };
template<class Typew,class Typep> class Knap;
class bbnode { friend Knap<int,int>; template<class Typew,class Typep> friend Typep Knapsack(Typep p[],Typew w[],Typew c,int n, int bestx[]); private: bbnode * parent; //指向父節點的指針 bool LChild; //左兒子節點標識 };
template<class Typew,class Typep> class HeapNode { friend Knap<Typew,Typep>; public: operator Typep() const { return uprofit; } private: Typep uprofit, //節點的價值上界 profit; //節點所相應的價值 Typew weight; //節點所相應的重量 int level; //活節點在子集樹中所處的層序號 bbnode *ptr; //指向活節點在子集中相應節點的指針 };
template<class Typew,class Typep> class Knap { template<class Typew,class Typep> friend Typep Knapsack(Typep p[],Typew w[],Typew c,int n, int bestx[]); public: Typep MaxKnapsack(); private: MaxHeap<HeapNode<Typep,Typew>> *H; Typep Bound(int i); void AddLiveNode(Typep up,Typep cp,Typew cw,bool ch,int lev);
bbnode *E; //指向擴展節點的指針 Typew c; //背包容量 int n; //物品數
Typew *w; //物品重量數組 Typep *p; //物品價值數組 Typew cw; //當前重量
Typep cp; //當前價值 int *bestx; //最優解 };
template <class Type> inline void Swap(Type &a,Type &b);
template<class Type> void BubbleSort(Type a[],int n);
int main() { int n = 3;//物品數 int c = 30;//背包容量 int p[] = {0,45,25,25};//物品價值 下標從1開始 int w[] = {0,16,15,15};//物品重量 下標從1開始 int bestx[4];//最優解
cout<<"背包容量為:"<<c<<endl; cout<<"物品重量和價值分別為:"<<endl;
for(int i=1; i<=n; i++) { cout<<"("<<w[i]<<","<<p[i]<<") "; } cout<<endl;
cout<<"背包能裝下的最大價值為:"<<Knapsack(p,w,c,n,bestx)<<endl; cout<<"此背包問題最優解為:"<<endl; for(int i=1; i<=n; i++) { cout<<bestx[i]<<" "; } cout<<endl; return 0; }
template<class Typew,class Typep> Typep Knap<Typew,Typep>::Bound(int i)//計算節點所相應價值的上界 { Typew cleft = c-cw; //剩余容量高 Typep b = cp; //價值上界 //以物品單位重量價值遞減序裝填剩余容量 while(i<=n && w[i]<=cleft) { cleft -= w[i]; b += p[i]; i++; }
//裝填剩余容量裝滿背包 if(i<=n) { b += p[i]/w[i]*cleft; }
return b; }
//將一個活節點插入到子集樹和優先隊列中 template<class Typew,class Typep> void Knap<Typew,Typep>::AddLiveNode(Typep up,Typep cp,Typew cw,bool ch,int lev) { bbnode *b = new bbnode; b->parent = E; b->LChild = ch;
HeapNode<Typep,Typew> N; N.uprofit = up; N.profit = cp; N.weight = cw; N.level = lev; N.ptr = b;
H->Insert(N); }
//優先隊列式分支限界法,返回最大價值,bestx返回最優解 template<class Typew,class Typep> Typep Knap<Typew,Typep>::MaxKnapsack() { H = new MaxHeap<HeapNode<Typep,Typew>>(1000);
//為bestx分配存儲空間 bestx = new int[n+1];
//初始化 int i = 1; E = 0; cw = cp = 0; Typep bestp = 0;//當前最優值 Typep up = Bound(1); //價值上界
//搜索子集空間樹 while(i!=n+1) { //檢查當前擴展節點的左兒子節點 Typew wt = cw + w[i]; if(wt <= c)//左兒子節點為可行節點 { if(cp+p[i]>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); }
//取下一擴展節點 HeapNode<Typep,Typew> N; H->DeleteMax(N); E = N.ptr; cw = N.weight; cp = N.profit; up = N.uprofit; i = N.level; }
//構造當前最優解 for(int j=n; j>0; j--) { bestx[j] = E->LChild; E = E->parent; } return cp; }
//返回最大價值,bestx返回最優解 template<class Typew,class Typep> Typep Knapsack(Typep p[],Typew w[],Typew c,int n, int bestx[]) { //初始化 Typew W = 0; //裝包物品重量 Typep P = 0; //裝包物品價值
//定義依單位重量價值排序的物品數組 Object *Q = new Object[n]; for(int i=1; i<=n; i++) { //單位重量價值數組 Q[i-1].ID = i; Q[i-1].d = 1.0*p[i]/w[i]; P += p[i]; W += w[i]; }
if(W<=c) { return P;//所有物品裝包 }
//依單位價值重量排序 BubbleSort(Q,n);
//創建類Knap的數據成員 Knap<Typew,Typep> K; K.p = new Typep[n+1]; K.w = new Typew[n+1];
for(int i=1; i<=n; i++) { K.p[i] = p[Q[i-1].ID]; K.w[i] = w[Q[i-1].ID]; }
K.cp = 0; K.cw = 0; K.c = c; K.n = n;
//調用MaxKnapsack求問題的最優解 Typep bestp = K.MaxKnapsack(); for(int j=1; j<=n; j++) { bestx[Q[j-1].ID] = K.bestx[j]; }
delete Q; delete []K.w; delete []K.p; delete []K.bestx; return bestp; } template<class Type> void BubbleSort(Type a[],int n) { //記錄一次遍歷中是否有元素的交換 bool exchange; for(int i=0; i<n-1;i++) { exchange = false ; for(int j=i+1; j<=n-1; j++) { if(a[j]<=a[j-1]) { Swap(a[j],a[j-1]); exchange = true; } } //如果這次遍歷沒有元素的交換,那么排序結束 if(false == exchange) { break ; } } }
template <class Type> inline void Swap(Type &a,Type &b) { Type temp = a; a = b; b = temp; } 編譯並運行程序。 5. 0-1背包問題的分枝—限界算法的搜索過程與解空間樹 當n=3時,w={16,15,15}, p={45,25,25}, c=30。優先隊列式分支限界法:處理法則:價值大者優先。{}—>{A}—>{B,C}—>{C,D,E}—>{C,E}—>{C,J,K}—>{C}—>{F,G}—>{G,L,M}—>{G,M}—>{G}—>{N,O}—>{O}—>{}
6.算法的時間復雜性和空間復雜性
0-1背包問題不同算法時間復雜性和空間復雜性的比較 0-1背包問題的分枝—限界算法的時間復雜度為:O(n*2n),空間復雜度為:O(nm), 0-1背包問題的回溯法時間復雜度為:O(n*2n),與分枝—限界算法相同,而空間復雜度與分枝—限界算法不同,為:θ(n)。 |
測試數據: 背包容量為30 物品重量和價值分別為: 16,45 15,25 15,25 程序運行結果如下:
為了方便只取了一組簡單的測試數據,可能會對實驗結果造成影響,后面還要取更多的數據進行測試。 |