回溯演算法 --- 例題5.0-1揹包問題
阿新 • • 發佈:2021-12-21
一.問題描述
略
二.解題思路
0-1揹包問題是子集選取問題.一般情況下,0-1揹包問題是NP完全問題.
0-1揹包問題的解空間用子集樹來表示,解0-1揹包問題的回溯法和解裝載問題的回溯法十分相似.搜尋解空間樹時,只要其左兒子節點是一個可行節點,搜尋就進入其左子樹.當右子樹中有可能包含最優解時(即上界大於當前最優價值,試想一下,若上界都不大於當前最優價值,那麼在進入右子樹就根本不可能找到更優解,何況上界還是一種理想狀態下的最佳)才進入右子樹搜尋;否則將右子樹剪去.
設r是當前剩餘物品價值總和,cp是當前價值;bestp是當前最最優價值;
當 cp + r <= bestp時,可剪去右子樹.
計算右子樹中解的上界的更好方法是,將剩餘物品依照其單位重量價值排序
為了便於計算上界,可先將物品依其單位重量價值從大到小排序,此後只要按順序考察各物品即可.
在實現時,由Bound計算當前節點處的上界.類Knap的資料成員記錄解空間樹中的節點資訊,以減少引數傳遞及遞迴呼叫所需的棧空間.在解空間樹的當前擴充套件結點處,僅當要進入右子樹時才計算上界Bound,以判斷是否可將右子樹剪去.進入左子樹時不需要算上界,因為其上界與其父節點的上界相同.
具體程式碼如下:
// 0-1揹包問題回溯解法 #include<bits/stdc++.h> using namespace std; class Knap { friend int Knapsack(int *, int *, int, int); private: int Bound(int i); //計算上界 void Backtrack(int i); int c; //揹包容量 int n; //物品數 int *w; //物品重量陣列 int *p; //物品價值陣列 int *number; //排序後的編號情況 bool *select; //用來記錄物品的選擇情況 int cw; //當前重量 int cp; //當前價值 int bestp; //當前最優價值 }; void Knap::Backtrack(int i) { static int k = 1; if(i > n) //到達葉節點 { bestp = cp; cout<<"第"<<k++<<"次到達葉節點,得到的最優價值為:"<<bestp<<endl; cout<<"此次物品選擇情況為:"<<endl; for(int i=1; i<=n; i++) { if(select[i]==true) cout<<"選擇物品"<<number[i]<<": 重量"<<w[i]<<" 價值"<<p[i]<<endl; } return ; } if(cw + w[i] <= c) //進入左子樹 { cout<<"進入左子樹深入一層,將到達第"<<i+1<<"層"<<endl; cw += w[i]; cp += p[i]; select[i] = true; Backtrack(i+1); cout<<"從左子樹回溯一層,將到達第"<<i<<"層"<<endl; select[i] = false; cw -= w[i]; cp -= p[i]; } else cout<<"此時: cw:"<<cw<<" w[i]:"<<w[i]<<" 則cw+w[i]>c,不滿足約束條件,無法繼續向左,嘗試向右"<<endl; if(Bound(i+1) > bestp) //進入右子樹,此時右子樹上界大於當前最優價值,所以可能找到更優解. { cout<<"進入右子樹深入一層,將到達第"<<i+1<<"層"<<endl; Backtrack(i+1); cout<<"從右子樹回溯一層,將到達第"<<i<<"層"<<endl; } else cout<<"嘗試向右,計算得到Bound[i+1]:"<<Bound(i+1)<<" Bound(i+1)<=bestp,不滿足限界條件,直接剪枝"<<endl; } int Knap::Bound(int i) //計算上界 { int cleft = c - cw; //當前剩餘容量 int b = cp; //當前價值 while(i<=n && w[i]<=cleft) //以物品單位重量價值遞減序裝入揹包 { cleft -= w[i]; b += p[i]; i++; } if(i<=n) //裝滿揹包 b += p[i]*cleft/w[i]; return b; } class Object { friend int Knapsack(int *, int *, int, int); public: // int operator<=(Object a) const {return d >= a.d;} int ID; //編號,保證排序後還能找到原來自己的重量和價值,從而用來構造一個新的重量陣列以及價值陣列 float d; //單位重量價值 }; bool cmp(Object a, Object b) { return a.d >= b.d; } //template<class int, class int> int Knapsack(int *p, int *w, int c, int n) //為Knap::Backtrack初始化 { int W = 0; int P = 0; Object *Q = new Object[n+1]; 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) //能裝入所有物品,直接返回P return P; sort(Q, Q+n, cmp); //依物品單位重量價值排序 // Sort(Q, n); int *temp_Number = new int[n+1]; cout<<"排序後的陣列為:"; for(int i=1; i<=n; i++) { temp_Number[i] = Q[i-1].ID; cout<<Q[i-1].ID<<" "; } cout<<endl; Knap K; //利用處理好的Object物件來構建揹包物件 K.number = new int[n+1]; K.number = temp_Number; K.select = new bool[n+1]; for(int i=0; i<=n; i++) K.select[i] = false; K.p = new int[n+1]; K.w = new int[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; K.bestp = 0; K.Backtrack(1); delete[] Q; delete[] K.w; delete[] K.p; return K.bestp; } int main() { cout<<"請輸入揹包總容量:"; int c; while(cin>>c && c) { cout<<"請輸入物品總件數:"; int n; cin>>n; cout<<"請輸入每件物品的重量以及價值"<<endl; int *w = new int[n+1]; int *p = new int[n+1]; for(int i=1; i<=n; i++) { cout<<"物品"<<i<<":"; cin>>w[i]>>p[i]; } int ans = Knapsack(p, w, c, n); cout<<"最優裝載價值為:"<<ans<<endl; delete[] w; delete[] p; cout<<"請輸入揹包總容量:"; } system("pause"); return 0; }
執行結果如下:
由此畫出的子集樹圖為:
可以看到,剪枝函式的合理運用為我們省去了很多不必要的搜尋,提高了搜尋效率.
這也是回溯演算法設計的核心,前面說過,回溯演算法本質就是去遍歷所有的情況,但是它與窮舉法的差別就在於它有著剪枝函式來幫助它砍掉很多無效搜尋.所以說,正確的提煉出題目要求所給的剪枝函式十分重要.
參考畢方明老師《演算法設計與分析》課件.
歡迎大家訪問個人部落格網站---喬治的程式設計小屋,一起加油!