回溯法(演算法分析與設計)
0.回溯法的演算法框架
A.簡介
回溯法,又稱試探法。一般需要遍歷解空間,時間複雜度概況:子集樹Ω(2^n),排序樹Ω(n!),暴力法
B.回溯法解題三步驟
1)定義問題的解空間
如0-1揹包問題,當n=3時,解空間是(0,0,0)、(0,0,1)、(0,1,0)、(0,1,1)、(1,0,0)、(1,0,1)、(1,1,0)、(1,1,1)。1代表選擇該物品,0代表不選擇該物品
C.子集樹、排列樹及其他2)確定易搜尋的解空間結構
3)以深度優先方式搜尋解空間,並在搜尋過程中用剪枝函式避免無效搜尋
1)子集樹
a.概念
b.回溯法搜尋子集樹的一般演算法當所給問題是從n個元素的集合S中找出S滿足的某種性質的子集時,相應的解空間樹稱為子集樹。例如,0-1揹包問題,要求在n個物品的集合S中,選出幾個物品,使物品在揹包容積C的限制下,總價值最大(即集合S的滿足條件<容積C下價值最大>的某個子集)。
另:子集樹是從集合S中選出符合限定條件的子集,故每個集合元素只需判斷是否(0,1)入選,因此解空間應是一顆滿二叉樹
void backtrack(int t)//t是當前層數 { if(t>n)//需要判斷每一個元素是否加入子集,所以必須達到葉節點,才可以輸出 { output(x); } else { for(int i=0;i<=1;i++)//子集樹是從集合S中,選出符合限定條件的子集,故每個元素判斷是(1)否(0)選入即可(二叉樹),因此i定義域為{0,1} { x[t]=i;//x[]表示是否加入點集,1表示是,0表示否 if(constraint(t)&&bound(t))//constraint(t)和bound(t)分別是約束條件和限定函式 { backtrack(t+1); } } } }
2)排列樹
a.概念
b.回溯法搜素排列樹的一般演算法當問題是確定n個元素滿足某種性質的排列時,相應的解空間稱為排列樹。排列樹與子集樹最大的區別在於,排列樹的解包括整個集合S的元素,而子集樹的解則只包括符合條件的集合S的子集。
void backtrack(int t)//t是當前層數 { if(t>n)//n是限定最大層數 { output(x); } else { for(int i=t;i<=n;i++)//排列樹的節點所含的孩子個數是遞減的,第0層節點含num-0個孩子,第1層節點含num-1個孩子,第二層節點含num-2個孩子···第num層節點為葉節點,不含孩子。即第x層的節點含num-x個孩子,因此第t層的i,它的起點為t層數,終點為num,第t層(根節點為空節點,除外),有num-t+1個親兄弟,需要輪num-t+1回 { swap(x[t],x[i]);//與第i個兄弟交換位置,排列樹一條路徑上是沒有重複節點的,是集合S全員元素的一個排列,故與兄弟交換位置後就是一個新的排列 if(constraint(t)&&bound(t))//constraint(t)和bound(t)分別是約束條件和限定函式 { backtrack(t+1); } swap(x[i],x[t]); } } }
3)解空間非子集樹,非排列樹
遞迴:
迭代:void backtrack(int t) { if(t>n) { output(x); } else { for(int i=f(n,t);i<=g(n,t);i++) { x[t]=h(i); if(constraint(t)&&bound(t)) { backtrack(t+1); } } } }
void backtrack(int t) { int t=1; while(t>0) { if(f(n,t)<=g(n,t)) { for(int i=f(n,t);i<=g(n,t);i++)//f和g是當前擴充套件節點的起止點 { x[t]=h(i); if(constraint(t)&&bound(t)) { if(solution(t))//原先的遞迴出口 { output(x); } else { t++;//未解決問題,但在限定條件之內,走下一層 } } } } else { t--;//回到上一層 } } }
D.方法
先判斷問題是子集樹、排列樹、還是非子集樹非排列樹,然後在做
1.裝載問題
A.遞迴
根據節1給出的回溯模板,模仿所得,子集樹
在編寫時未考慮最優解問題(C1上裝載越多,解越優),故本程式無法剪枝。若要考慮最優解,當總載重量小於最優載重量時,可以把右子樹(0,不選擇)全部剪去
#include<iostream> #include<vector> #include<cstdlib> using namespace std; int c1,c2,wsum; vector<int> w; vector<int> s;//是否加入點集C1 int random(int start,int end) { return start+rand()%(end-start); } int s1=0; void maxLoading(int c1w,int t) { if(t>=w.size()) { if((wsum-c1w)<=c2) { cout<<"choice "<<++s1<<":\n\tc1 :"; for(int i=0;i<s.size();i++) { if(s[i]==1) { cout<<i<<"\t"; } } cout<<endl; } } else { for(int i=0;i<=1;i++) { s[t]=i; if(i==1) { c1w+=w[t]; } if(c1w<=c1) { maxLoading(c1w,t+1); } } } } int main() { int num; cin>>num>>c1>>c2; wsum=0; for(int i=0;i<num;i++) { int temp=random(1,100); w.push_back(temp); s.push_back(-1); wsum+=temp; cout<<temp<<"\t"; } cout<<endl; maxLoading(0,0); return 0; }
課本中和上述其實是一樣的,一個用for,i=0,i=1來訪問左右子樹,棄用for,直接上
B.迭代void maxLoading(int c1w,int t) { if(t>=w.size()) { if((wsum-c1w)<=c2) { cout<<"choice "<<++s1<<":\n\tc1 :"; for(int i=0;i<s.size();i++) { if(s[i]==1) { cout<<i<<"\t"; } } cout<<endl; } } else { if(c1w+w[t]<=c1)//左子樹 { c1w+=w[t]; s[t]=1; maxLoading(c1w,t+1); c1w-=w[t]; } s[t]=0; maxLoading(c1w,t+1);//右子樹 } }
其實就是二叉樹遍歷的非遞迴版本(直接爬的程式碼)
template <class Type> Type MaxLoading(Type w[],Type c,int n,int bestx[]) { //迭代回溯法,返回最優裝載量及其相應解,初始化根節點 int i =1; int *x = new int[n+1]; Type bestw = 0, cw = 0, r = 0; for(int j=1;j<=n;j++) r+=w[j]; while(true) { while(i<=n && cw+w[i]<=c) { r -= w[i]; cw +=w[i]; x[i] =1; i++; } if(i>n) { for(int j=1;j<=n;j++) bestx[j] = x[j]; bestw = cw; } else { r -= w[i]; x[i] = 0; i++; } while(cw+w[i] <= bestw) { i--; while(i>0 && !x[i]) { r+=w[i]; i--; } if(i == 0) { delete[] x; return bestw; } x[i] =0; cw -= w[i]; i++; } } }
2.批處理作業排程
排列樹
#include<iostream> #include<vector> #include<cstdlib> #include<climits> #include<algorithm> using namespace std; struct Node { int tm1; int tm2; }; vector<int> x;//當前路徑 vector<int> bestx;//最優路徑 vector<int> f2;//機器2完成處理時間 vector<Node> v;//每個作業需要的處理時間 int bestT; int f;//完成時間和 int f1;//機器1完成處理時間 int num;//作業數 int random(int start,int end) { return start+rand()%(end-start); } void flowShop(int t)//t>=1 { if(t>num) { for(int i=1;i<=num;i++) { bestx[i]=x[i]; } bestT=f; } else { for(int i=t;i<=num;i++) { f1+=v[x[i]].tm1; f2[t]=((f2[t-1]>f1)?f2[t-1]:f1)+v[x[i]].tm2; f+=f2[t]; swap(x[t],x[i]); if(f<bestT)//剪枝剪去f>=bestT的分支 { flowShop(t+1); } swap(x[i],x[t]); f1-=v[x[i]].tm1; f-=f2[t]; } } } int main() { cin>>num; for(int i=0;i<=num;i++) { Node t; t.tm1=random(1,10); t.tm2=random(1,10); v.push_back(t); x.push_back(i);//初始化路徑為作業輸入順序 f2.push_back(0); bestx.push_back(-1); } f=0; f1=0; bestT=INT_MAX; flowShop(1); for(int i=1;i<=num;i++) { cout<<bestx[i]<<"\t"; } cout<<"\n"<<bestT<<endl; }
3.符號三角形問題
行一的每個元素都具有兩種選擇(0,+,1,-),行n的符號由行n-1來決定。
子集樹,未剪枝
#include<iostream> #include<vector> using namespace std; vector<int> v; int num; int countSame=0; bool isEqual() { int count0=0; int k=num; for(int j=num;j>0;j--)//確定符號 { for(int i=k;i<k+j-1;i++) { v[i]=(v[i-j]==v[i-j+1])?0:1; } k+=j-1; } for(int i=0;i<(num+1)*num/2;i++)//統計減號個數 { if(v[i]) { count0++; } } if(count0*2==num*(num+1)/2)//是否相等 { return true; } return false; } void Triangels(int t) { if(t>=num) { if(isEqual())//相等加1 { countSame++; } } else { for(int i=0;i<=1;i++)//行一num個元素,均有兩種選擇 { v[t]=i; Triangels(t+1); } } } int main() { cin>>num; for(int i=0;i<(num+1)*num/2;i++) { v.push_back(-1); } Triangels(0); cout<<countSame<<endl; }
4.n後問題
n後問題,解空間非子集樹,非排列樹(一直往這方向上靠,做了上面3個題,有些先入為主了)
每行皇后只有一位,用v[ i ] = j來表示皇后在( i , j ),確實沒有想到(一直考慮用二維陣列,或者一維陣列模擬二維陣列)
用斜率來判斷是否在一條斜線上,這個也沒想到
A.遞迴
#include<iostream> #include<vector> #include<cmath> using namespace std; vector<int> v;//v[i]=j,表示第i行,第j列是皇后 int num;//棋盤行長,列長,也是皇后個數 int sum;//可行方案個數 bool isOk(int t) { for(int j=1;j<t;j++)//待選皇后v[t],是否與t行之前的皇后有衝突(每行一個皇后) { if(v[t]==v[j]||abs(t-j)==abs(v[t]-v[j]))//用斜率來判斷是否在一條斜線上!!! { return false; } } return true; } void nQueen(int t) { if(t>num) { sum++; } else { for(int i=1;i<=num;i++) { v[t]=i;//第t行的皇后放在第i列 if(isOk(t)) { nQueen(t+1); } } } } int main() { sum=0; cin>>num; for(int i=0;i<=num;i++) { v.push_back(0); } nQueen(1); cout<<sum<<endl; }
B.迭代
#include<iostream> #include<vector> #include<cmath> using namespace std; vector<int> v;//v[i]=j,表示第i行,第j列是皇后 int num;//棋盤行長,列長,也是皇后個數 int sum;//可行方案個數 bool isOk(int t) { for(int j=1;j<t;j++)//待選皇后v[t],是否與t行之前的皇后有衝突(每行一個皇后) { if(v[t]==v[j]||abs(t-j)==abs(v[t]-v[j]))//用斜率來判斷是否在一條斜線上!!! { return false; } } return true; } void nQueen(int t) { v[t]=0; while(t>0) { v[t]+=1;//第t行,第v[t]+1列 while(!isOk(t)&&v[t]<=num)//列範圍內,尋找符合條件的v[t]=j { v[t]+=1; } if(v[t]<=num)//找到 { if(t==num) { sum++;//找到後,下個迴圈v[t]會大於num,有t--處理 } else { t++; v[t]=0; } } else//未找到 { t--; } } } int main() { sum=0; cin>>num; for(int i=0;i<=num;i++) { v.push_back(0); } nQueen(1); cout<<sum<<endl; }
5.0-1揹包問題
子集樹
#include<iostream> #include<vector> #include<cstdlib> using namespace std; struct Node { int w; int p; }; vector<Node> v;//物品 vector<int> x;//當前方案 vector<int> bestx;//儲存最佳方案 int num;//物品數 int c;//揹包容量 int maxP;//最大價值 int random(int start,int end) { return start+rand()%(end-start); } void storage(int cp) { if(cp>maxP) { maxP=cp; for(int i=0;i<num;i++) { bestx[i]=x[i]; } } } void knapsack(int cw,int t,int cp) { if(t>=num) { return; } else { if(cw<=c) { for(int i=0;i<=1;i++) { x[t]=i; if(i==1) { cw+=v[t].w; cp+=v[t].p; } if(cw<=c) { storage(cp); knapsack(cw,t+1,cp); } } } } } int main() { maxP=-1; cin>>num>>c; for(int i=0;i<num;i++) { Node temp; temp.w=random(1,20); temp.p=random(1,100); v.push_back(temp); x.push_back(0); bestx.push_back(0); } knapsack(0,0,0); cout<<maxP<<endl; }
6.最大團問題
子集樹
#include<iostream> #include<vector> using namespace std; vector< vector<int> > v; vector<int> x; vector<int> bestx; int maxv;//最優解頂點個數 int num; void storage(int nowv) { if(nowv>maxv) { maxv=nowv; for(int i=0;i<num;i++) { bestx[i]=x[i]; } } } bool judge(int t)//是否都與t相連 { for(int i=0;i<t;i++) { if(x[i]==1&&v[t][i]==0) { return false; } } return true; } void MaxClique(int t,int nowv) { if(t>=num) { storage(nowv); } else { for(int i=0;i<=1;i++) { x[t]=i; if(i==0) { MaxClique(t+1,nowv); } else if(i==1&&judge(t)) { MaxClique(t+1,nowv+1); } } } } int main() { maxv=-1; cin>>num; //初始化全無邊 for(int i=0;i<num;i++) { vector<int> temp; for(int j=0;j<num;j++) { temp.push_back(0); } v.push_back(temp); x.push_back(-1); bestx.push_back(-1); } //輸入邊 while(true) { int x1,y1; if(cin>>x1>>y1) { v[x1][y1]=1; v[y1][x1]=1; } else { break; } } MaxClique(0,0); cout<<maxv<<endl; for(int i=0;i<num;i++) { if(bestx[i]==1) { cout<<i+1<<"\t"; } } cout<<endl; }
7.圖的m著色問題
子集樹,解空間子集樹,不是非要往0-1二叉樹上靠,也可以是m叉樹
#include<iostream> #include<vector> using namespace std; vector< vector<int> > v; vector<int> x; int num; int m; int sum; bool judge(int t,int color) { for(int i=0;i<t;i++) { if(x[i]==color&&v[i][t]==1) { return false; } } return true; } void coloring(int t) { if(t>=num) { cout<<sum<<": "; for(int i=0;i<num;i++) { cout<<x[i]<<"\t"; } cout<<endl; sum++; } else { for(int i=0;i<m;i++) { x[t]=i; if(judge(t,i)) { coloring(t+1); } x[t]=0; } } } int main() { sum=0; cin>>num>>m; for(int i=0;i<num;i++) { vector<int> temp; for(int j=0;j<num;j++) { temp.push_back(0); } v.push_back(temp); x.push_back(-1); } while(true) { int x1,y1; if(cin>>x1>>y1) { v[x1][y1]=1; v[y1][x1]=1; } else { break; } } coloring(0); cout<<sum<<endl; }
8.旅行售貨員問題
排列樹
#include<iostream> #include<vector> #include<climits> #include<algorithm> #include<cstdlib> using namespace std; vector< vector<int> > v; vector<int> x; int num; int costbest; int random(int s,int e) { return s+rand()%(e-s); } int countDis() { int cost=0; for(int i=1;i<x.size();i++) { cost+=v[x[i-1]][x[i]]; } return cost+v[x[0]][x[x.size()-1]]; } void Bttsp(int firstcity,int t) { if(t>=num) { int costx=countDis(); if(costx<costbest) { costbest=costx; } } else { for(int i=t;i<num;i++) { if(x[0]==firstcity) { swap(x[t],x[i]); Bttsp(firstcity,t+1); swap(x[t],x[i]); } } } } int main() { costbest=INT_MAX; cin>>num; for(int i=0;i<num;i++) { vector<int> temp; for(int j=0;j<num;j++) { temp.push_back(0); } v.push_back(temp); x.push_back(i); } for(int i=0;i<num;i++) { for(int j=i+1;j<num;j++) { int temp=random(1,50); v[i][j]=temp; v[j][i]=temp; cout<<"v["<<i<<"]["<<j<<"]="<<temp<<endl; } } Bttsp(0,0); cout<<costbest<<endl; }
9.圓排列問題
排列樹
#include<iostream> #include<vector> #include<cmath> #include<climits> #include<algorithm> using namespace std; vector<int> x; vector<int> r; int num; double disbest; double countDis(int a,int b) { return sqrt(pow(a+b+0.0,2)-pow(a-b+0.0,2)); } void circle(int t,double discost) { if(t>=num) { if(discost<disbest) { disbest=discost; } } else { for(int i=t;i<num;i++) { swap(x[t],x[i]); double temp; if(t+1==num) { temp=r[x[t]]; } else { temp=countDis(r[x[t]],r[x[t+1]]); } if(discost+temp<disbest) { circle(t+1,discost+temp); } swap(x[i],x[t]); } } } void change() { int temp=x[0]; for(int i=1;i<num;i++) { x[i-1]=x[i]; } x[num-1]=temp; } int main() { disbest=INT_MAX; cin>>num; for(int i=0;i<num;i++) { int temp; cin>>temp; r.push_back(temp); x.push_back(i); } for(int i=0;i<num;i++) { circle(0,r[x[0]]+0.0); change(); } cout<<disbest<<endl; }