算法设计与分析第5章 回溯法(二)【回溯法应用】
第5章 回溯法
5.2 應用范例
1.0-1背包問題
有n件物品和一個容量為c的背包。第i件物品的重量是w[i],價值是p[i]。求解將哪些物品裝入背包可使這些物品的重量總和不超過背包容量,且價值總和最大。
·算法設計:
(1)解空間:子集樹
(2)算法調用遞歸函數Backtrack
(3)可行性約束函數:
(4)上界函數:當前包內物品重量(cw)+物品的選中情況(x[i])*物品的重量(w[i])<=背包的容量?
(5)最后構造最優解
·代碼實現:
#include<stdio.h>
int n,c,bestp;//物品的個數,背包的容量,最大價值
int p[10000],w[10000],x[10000],bestx[10000];//物品的價值,物品的重量,x[i]暫存物品的選中情況,物品的選中情況void Backtrack(int i,int cp,int cw)//cw當前包內物品重量,cp當前包內物品價值
{if(i>n) //回溯結束{if(cp>bestp) //當前重量優于最優解{bestp=cp; //更新最優解與最優值for(i=0; i<=n; i++){bestx[i]=x[i];}}}else //訪問左、右子樹{for(int j=0; j<=1; j++){x[i]=j; //j=1訪問左子樹,j=0訪問右子樹if(cw+x[i]*w[i]<=c) //限界函數{cw+=w[i]*x[i];cp+=p[i]*x[i];Backtrack(i+1,cp,cw);cw-=w[i]*x[i];cp-=p[i]*x[i];}}}
}int main()
{int i;bestp=0;printf("請輸入背包最大容量:\n");scanf("%d",&c);printf("請輸入物品個數:\n");scanf("%d",&n);printf("請依次輸入物品的重量:\n");for(i=1; i<=n; i++)scanf("%d",&w[i]);printf("請依次輸入物品的價值:\n");for(i=1; i<=n; i++)scanf("%d",&p[i]);Backtrack(1,0,0);printf("最大價值為:\n");printf("%d\n",bestp);printf("被選中的物品依次是(0表示未選中,1表示選中):\n");for(i=1; i<=n; i++)printf("%d ",bestx[i]);printf("\n");return 0;
}
/*
樣例:
input請輸入背包最大容量:
70
請輸入物品個數:
4
請依次輸入物品的重量:
20 15 25 30
請依次輸入物品的價值:
60 25 55 60output最大價值為:
145
被選中的物品依次是(0表示未選中,1表示選中):
1 1 0 1
*/
2.裝載問題
有一批共n個集裝箱要裝上2艘載重量分別為c1和c2的輪船,其中集裝箱i的重量為wi,且
裝載問題要求確定是否有一個合理的裝載方案可將這些集裝箱裝上這2艘輪船。如果有,找出一種裝載方案。
·問題分析:
容易證明,如果一個給定裝載問題有解,則采用下面的策略可得到最優裝載方案。
(1)首先將第一艘輪船盡可能裝滿;
(2)將剩余的集裝箱裝上第二艘輪船。
將第一艘輪船盡可能裝滿等價于選取全體集裝箱的一個子集,使該子集中集裝箱重量之和最接近。由此可知,裝載問題等價于以下特殊的0-1背包問題。
·復雜度:
用回溯法設計解裝載問題的O(2n)計算時間算法。在某些情況下該算法優于動態規劃算法。
·算法設計:
解空間:子集樹最合適。
(4)算法MaxLoading調用遞歸函數Backtrack
(5)可行性約束函數(選擇當前元素):
(3)上界函數,減去不含最優解的子樹:當前載重量(cw)+剩余集裝箱的重量?<=當前最優載重量(bestw)
(4)最后構造最優解
·代碼實現:
#include <iostream>
using namespace std;typedef int* pINT;
template<class Type>
class Loading
{
public:friend Type MaxLoading(Type* w,int num ,Type C1,int* bestx);friend void SolveLoading(int C2,bool* x,int* w,int num);void Backtrack(int i);int num; //集裝箱數目int * x; //當前解int * bestx; //當前最優解Type* w; //集裝箱重量數組Type C1; //第一艘船的容量Type cw; //當前載重量Type bestw; //當前最優載重量Type r; //剩余集裝箱重量
};//搜索第i層結點
template<class Type>
void Loading<Type>::Backtrack(int i)
{//[1]到達葉結點if(i > num){if (cw > bestw) //當前重量優于最優解{for (int i = 1; i <= num ; i++){bestx[i] = x[i]; //更新最優解與最優值bestw = cw;}}return ;}//[2]搜索子樹r -= w[i];if (cw+w[i] <= C1) //搜索左子樹{x[i] = 1;cw += w[i];Backtrack(i+1);cw -= w[i]; //恢復狀態}if (cw+r > bestw) //可能存在最優解,搜索右子樹(剪枝條件:cw+r<=bestw){x[i] = 0;Backtrack(i+1);}r += w[i]; //恢復狀態
}//求解最優裝載
template<class Type>
Type MaxLoading(Type* w,int num ,Type C1,int* bestx)
{Loading<Type> X;//初始化XX.x = new int[num+1];X.w = w;X.C1= C1;X.num = num;X.bestx = bestx;X.bestw = 0;X.cw = 0;X.r = 0;for (int i = 1; i <= num ; i++){X.r += w[i];}X.Backtrack(1);delete[] X.x;return X.bestw;
}
//輸出最優裝載
template<class Type>
void SolveLoading(int C2,int* x,Type* w,int num)
{int totalW = 0;int c1W = 0; //第一艘船總載重for (int i = 1; i <= num ; i++){if (x[i] == 1){c1W += w[i];}totalW += w[i];}if (totalW-c1W > C2){cout<<"沒有合理的裝載方案! :( ";return;}cout<<"裝載方案如下:\n";cout<<"第一艘船裝: ";for (int i = 1; i <= num ; i++){if ( x[i] == 1 ){cout<<i<<" ";}}cout<<"\n總載重:"<<c1W<<"\n";cout<<"第二艘船裝: ";for (int i = 1; i <= num ; i++){if ( ! x[i] ){cout<<i<<" ";}}cout<<"\n總載重:"<<totalW-c1W<<"\n";
}int main(int argc,char* argv[])
{int C1 = 0;int C2 = 0;int num = 0;int* x = NULL;int** m = NULL;int* w = NULL;cout<<"輸入第一艘船最大載重量:";cin>>C1;cout<<"輸入第二艘船最大載重量:";cin>>C2;cout<<"輸入貨物個數:";cin>>num;x = new int[num+1];w = new int[num+1];m = new pINT[num+1];for (int i = 0; i < num+1 ; i++){m[i] = new int[num+1];}cout<<"分別輸入貨物重量(回車結束):\n";for (int i = 1; i <= num ; i++){cin>>w[i];}MaxLoading(w, num, C1, x);SolveLoading(C2, x, w, num);delete[] x;delete[] w;delete[] m;return 0;
}
/*
測試樣例:
input輸入第一艘船最大載重量:150
輸入第二艘船最大載重量:100
輸入貨物個數:7
分別輸入貨物重量(回車結束):
10 35 25 30 40 55 15output
裝載方案如下:
第一艘船裝: 1 4 5 6 7
總載重:150
第二艘船裝: 2 3
總載重:60
*/
3.符號三角形問題
下圖是由14個“+”和14個“-”組成的符號三角形。2個同號下面都是“+”,2個異號下面都是“-”。
在一般情況下,符號三角形的第一行有n個符號。符號三角形問題要求對于給定的n,計算有多少個不同的符號三角形,使其所含的“+”和“-”的個數相同。
·算法設計:
(1)解向量:用n元組x[1:n]表示符號三角形的第一行
(2)算法調用遞歸函數Backtrack
(3)可行性約束函數:當前符號三角形所包含的“+”個數與“-”個數均不超過n*(n+1)/4
(4)無解的判斷:n*(n+1)/2為奇數
(5)構造最優解
·代碼實現:
#include <iostream>
#include <string.h>
using namespace std;char cc[2]= {'+','-'}; //便于輸出
int n; //第一行符號總數
int half; //全部符號總數一半
int counter; //1計數,即“-”號計數
int **p; //符號存儲空間
long sum; //符合條件的三角形計數void Backtrace(int t) //第一行第t個符號
{int i,j;if(t > n) //符號填充完畢,打印符號{sum++; //三角形計數加1cout<<"\n第"<<sum<<"個三角形:"<<endl;for(i=1; i<=n; i++){for(j=1; j<i; j++){cout << " ";}for(j=1; j<=n-i+1; j++){cout << cc[ p[i][j] ] << " ";}cout << endl;}}else{for(i=0; i<2; i++){p[1][t] = i; //第一行第t個符號counter += i; //“-”號統計,因為"+"的值是0for(j=2; j<=t; j++) //當第一行符號>=2時,可以運算出下面行的某些符號,j可代表行號{p[j][t-j+1] = p[j-1][t-j+1]^p[j-1][t-j+2]; //通過異或運算下行符號,t-j+1確定的很巧counter += p[j][t-j+1];}//剪枝[2](限界函數):若符號統計未超過半數,并且另一種符號也未超過半數,同時隱含兩者必須相等才能結束if((counter <= half) && (t*(t+1)/2 - counter <= half)){Backtrace(t+1); //在第一行增加下一個符號}//回溯,判斷另一種符號情況,像是出棧一樣,恢復所有對counter的操作for(j=2; j<=t; j++){counter -= p[j][t-j+1];}counter -= i;}}
}int main()
{cout << "請輸入第一行符號個數n:";cin >> n;counter = 0;sum = 0;half = n*(n+1)/2;int i;//剪枝[1](約束函數):總數須為偶數,若為奇數則無解if( half%2 == 0 ){half /= 2;p = new int *[n+1];for(i=0; i<=n; i++){p[i] = new int[n+1];memset(p[i], 0, sizeof(int)*(n+1));}Backtrace(1);for(i=0; i<=n; i++) //刪除二維動態數組的方法{delete[] p[i];}delete[] p;}cout<<"-----------------------------"<<endl;cout<<"共有"<<sum<<"個符號三角形"<<endl;return 0;
}
4.旅行售貨員問題
某售貨員要到若干個城市去推銷商品,已知各個城市之間的路程(旅費)。他要選擇一條從駐地出發,必須經過各個城市且僅一遍,最后返回到駐地的總成本(路程或費用等)最小路線。
即在帶權圖G=(V,E)中尋找一條成本最小的周游路線。
·算法設計:
(1)解空間:一棵排列樹
(2)算法調用遞歸函數Backtrack
(3)剪枝函數:x[1:i]的費用小于當前最優值時算法進入樹的第i層,否則將剪去相應子樹。
(4)構造最優解
·算法分析:
對于排列樹的回溯法與生成1,2,……n的所有排列的遞歸算法類似。開始時x=[1,2,……n],則相應的排列樹有x[1:n]的所有排列構成。
在遞歸算法Backtrack中,
(1)當i=n時,當前擴展節點是排列樹的葉節點的父節點。此時算法檢測圖G是否存在一條從頂點x[n-1]到頂點x[n]的邊和一條從頂點x[n]到頂點1的邊。如果這兩條邊都存在,則找到一條旅行員售貨回路。此時,算法還需要判斷這條回路的費用是否優于已找到的當前最優回流的費用bestc。如果是,則必須更新當前最優值bestc和當前最優解bestx。
(2)當i<n時,當前擴展節點位于排列樹的第i-1層。圖G中存在從頂點x[i-1]到頂點x[i]的邊時,x[1:i]構成圖G的一條路徑,且當x[1:i]的費用小于當前最優值時算法進入樹的第i層,否則將剪去相應的子樹。
·復雜度分析:
算法backtrack在最壞情況下可能需要更新當前最優解O((n-1)!)次,每次更新bestx需計算時間O(n),從而整個算法的計算時間復雜性為O(n!)。
·代碼實現:
#include <iostream>
using namespace std;
#define N 4
#define NoEdge -1 //無邊標記-1
class Traveling
{friend double TSP(double (*a)[N+1], int n);
private:void Backtrack(int i);void Swap(int &x, int &y);int n; //圖G的頂點數int *x; //當前解int *bestx; //最優解,保存全排列中最優的解double (*a)[N+1]; //圖G的鄰接矩陣double cc; //當前費用double bestc; //當前最優值bool iscycle; //判斷是否有回路
};
void Traveling::Backtrack(int i) //對數組x中第i起到結尾進行全排列的試探,數組x下標為0的元素保留不用
{if(i == n) //找到符合條件的全排列{if (a[x[i-1]][x[i]] != NoEdge && a[x[i]][x[1]] != NoEdge && (bestc > cc + a[x[i-1]][x[i]] +a[x[i]][x[1]] || bestc == NoEdge)) //判斷是否有回路、發現最優值{iscycle = true;bestc = cc + a[x[i-1]][x[i]] +a[x[i]][x[1]]; //保存最優值for (int i = 1; i <= n; i++){bestx[i] = x[i]; //保存最優解}}}else{for (int j =i; j <= n; j++){if(a[x[i-1]][x[j]] != NoEdge && (cc + a[x[i-1]][x[j]] < bestc || bestc == NoEdge))// 是否可進入x[j]子樹{// 搜索子樹Swap(x[i],x[j]);cc += a[x[i-1]][x[i]]; //當前費用累加Backtrack(i+1); //排列向右擴展,排列樹向下一層擴展cc -= a[x[i-1]][x[i]];Swap(x[i],x[j]);}}}
}
void Traveling::Swap(int &x, int &y)
{int temp;temp = x;x = y;y= temp;
}
double TSP(double (*a)[N+1], int n)
{Traveling T;//初始化TT.bestc = NoEdge;T.cc = 0;T.n = n;T.x = new int[n+1];T.bestx = new int[n+1];T.a = a;T.iscycle = false;//置x為單位排列for (int i = 1; i <= n; i++){T.x[i] = i;}T.Backtrack(2); //以T.x數組中下標為1的頂點作為旅行售貨員的出發點。if (T.iscycle){cout<<"\n旅行售貨員的最優回路代價:"<<T.bestc<<endl<<"旅行售貨員的最優回路路徑:";for (int i = 1; i <= n; i++){cout<<T.bestx[i]<<" ";}cout<<1<<endl;}elsecout<<"圖中無回路!"<<endl;delete [] T.x;delete [] T.bestx;return T.bestc;
}
int main(int argc, char* argv[])
{//對圖a初始化double a[N+1][N+1];a[1][2] = 30;a[1][3] = 6;a[1][4] = 4;a[2][3] = 5;a[2][4] = 10;a[3][4] = 20;for (int i = 1; i <= N; i++){for (int j = i + 1; j<= N; j++){a[j][i] = a[i][j];}}cout<<"圖的頂點個數 n="<<N<<endl;TSP(a, N);return 0;
}
5.圓排列問題
給定n個大小不等的圓c1,c2,…,cn,現要將這n個圓排進一個矩形框中,且要求各圓與矩形框的底邊相切。圓排列問題要求從n個圓的所有排列中找出有最小長度的圓排列。例如,當n=3,且所給的3個圓的半徑分別為1,1,2時,這3個圓的最小長度的圓排列如圖所示。其最小長度為
·算法設計:
(5)解空間:一棵排列樹
(6)算法調用遞歸函數Backtrack
(7)剪枝函數:x[1:i]的費用小于當前最優值時算法進入樹的第i層,否則將剪去相應子樹。
(8)構造最優解
·算法分析:
按照回溯法搜索排列樹的算法框架,設開始時a=[r1,r2,……rn]是所給的n個元的半徑,則相應的排列樹由a[1:n]的所有排列構成。
解圓排列問題的回溯算法中,
(1)初始時,數組a是輸入的n個圓的半徑,計算結束后返回相應于最優解的圓排列。
(2)center計算圓在當前圓排列中的橫坐標,由x2 = sqrt((r1+r2)2-(r1-r2)2)推導出x = 2*sqrt(r1*r2)。
(3)Compoute計算當前圓排列的長度。變量min記錄當前最小圓排列長度。數組r表示當前圓排列。
(4)數組x則記錄當前圓排列中各圓的圓心橫坐標。
在遞歸算法Backtrack中,
(1)當i>n時,算法搜索至葉節點,得到新的圓排列方案。此時算法調用Compute計算當前圓排列的長度,適時更新當前最優值。
(2)當i<n時,當前擴展節點位于排列樹的i-1層。此時算法選擇下一個要排列的圓,并計算相應的下界函數。
·復雜度分析:
由于算法backtrack在最壞情況下可能需要計算O(n!)次當前圓排列長度,每次計算需O(n)計算時間,從而整個算法的計算時間復雜性為O((n+1)!)
·代碼實現:
//圓排列問題 回溯法求解
#include <iostream>
#include <cmath>
using namespace std;float CirclePerm(int n,float *a);template <class Type>
inline void Swap(Type &a, Type &b);int main()
{float *a = new float[4];a[1] = 1,a[2] = 1,a[3] = 2;cout<<"圓排列中各圓的半徑分別為:"<<endl;for(int i=1; i<4; i++){cout<<a[i]<<" ";}cout<<endl;cout<<"最小圓排列長度為:";cout<<CirclePerm(3,a)<<endl; //樣例結果為2+4*sqrt(2)return 0;
}class Circle
{friend float CirclePerm(int,float *);
private:float Center(int t); //計算當前所選擇的圓在當前圓排列中圓心的橫坐標void Compute(); //計算當前圓排列的長度void Backtrack(int t);float min, //當前最優值*x, //當前圓排列圓心橫坐標*r; //當前圓排列int n; //圓排列中圓的個數
};// 計算當前所選擇圓的圓心橫坐標
float Circle::Center(int t)
{float temp=0;for (int j=1; j<t; j++){//由x^2 = sqrt((r1+r2)^2-(r1-r2)^2)推導而來float valuex=x[j]+2.0*sqrt(r[t]*r[j]);if (valuex>temp){temp=valuex;}}return temp;
}// 計算當前圓排列的長度
void Circle::Compute(void)
{float low=0,high=0;for (int i=1; i<=n; i++){if (x[i]-r[i]<low){low=x[i]-r[i];}if (x[i]+r[i]>high){high=x[i]+r[i];}}if (high-low<min){min=high-low;}
}void Circle::Backtrack(int t)
{if (t>n){Compute();}else{for (int j = t; j <= n; j++){Swap(r[t], r[j]);float centerx=Center(t);if (centerx+r[t]+r[1]<min) //下界約束{x[t]=centerx;Backtrack(t+1);}Swap(r[t], r[j]);}}
}float CirclePerm(int n,float *a)
{Circle X;X.n = n;X.r = a;X.min = 100000;float *x = new float[n+1];X.x = x;X.Backtrack(1);delete []x;return X.min;
}template <class Type>
inline void Swap(Type &a, Type &b)
{Type temp=a;a=b;b=temp;
}
6.連續郵資問題
假設國家發行了n種不同面值的郵票,并且規定每張信封上最多只允許貼m張郵票。連續郵資問題要求對于給定的n和m的值,給出郵票面值的最佳設計,在1張信封上可貼出從郵資1開始,增量為1的最大連續郵資區間。
例如,當n=5和m=4時,面值為(1,3,11,15,32)的5種郵票可以貼出郵資的最大連續郵資區間是1到70。
·算法設計:
(1)解向量:用n元組x[1:n]表示n種不同的郵票面值,并約定它們從小到大排列。x[1]=1是唯一的選擇。
(2)可行性約束函數:已選定x[1:i-1],最大連續郵資區間是[1:r],接下來x[i]的可取值范圍是[x[i-1]+1:r+1]。
·算法分析:
如何確定r的值?
計算X[1:i]的最大連續郵資區間在本算法中被頻繁使用到,因此勢必要找到一個高效的方法。考慮到直接遞歸的求解復雜度太高,我們不妨嘗試計算用不超過m張面值為x[1:i]的郵票貼出郵資k所需的最少郵票數y[k]。通過y[k]可以很快推出r的值。事實上,y[k]可以通過遞推在O(n)時間內解決:
for (int j=0; j<= x[i-2]*(m-1);j++)if (y[j]<m)for (int k=1;k<=m-y[j];k++)if (y[j]+k<y[j+x[i-1]*k]) y[j+x[i-1]*k]=y[j]+k;while (y[r]<maxint) r++;
·算法實現:
//連續郵資問題 回溯法求解
#include <iostream>
using namespace std;class Stamp
{friend int MaxStamp(int ,int ,int []);private:int Bound(int i);void Backtrack(int i,int r);int n; //郵票面值數int m; //每張信封最多允許貼的郵票數int maxvalue; //當前最優值int maxint; //大整數int maxl; //郵資上界int *x; //當前解int *y; //貼出各種郵資所需最少郵票數int *bestx; //當前最優解
};int MaxStamp(int n,int m,int bestx[]);int main()
{int *bestx;int n = 5;int m = 4;cout<<"郵票面值數:"<<n<<endl;cout<<"每張信封最多允許貼的郵票數:"<<m<<endl;bestx=new int[n+1];for(int i=1;i<=n;i++){bestx[i]=0;}cout<<"最大郵資:"<<MaxStamp(n,m,bestx)<<endl;cout<<"當前最優解:";for(int i=1;i<=n;i++){cout<<bestx[i]<<" ";}cout<<endl;return 0;
}void Stamp::Backtrack(int i,int r)
{/**動態規劃方法計算數組y的值。狀態轉移過程:*考慮將x[i-1]加入等價類集S中,將會引起數組x*能貼出的郵資范圍變大,對S的影響是如果S中的*郵票不滿m張,那就一直貼x[i-1],直到S中有m張*郵票,這個過程會產生很多不同的郵資,取能產生*最多不同郵資的用郵票最少的那個元素*/for(int j=0;j<=x[i-2]*(m-1);j++){if(y[j]<m){for(int k=1;k<=m-y[j];k++)//k x[i-1]的重復次數{if(y[j]+k<y[j+x[i-1]*k]){y[j+x[i-1]*k]=y[j]+k;}}}}//如果y[r]的值在上述動態規劃運算過程中已賦值,則y[r]<maxintwhile(y[r]<maxint){r++;}if(i>n){if(r-1>maxvalue){maxvalue=r-1;for(int j=1;j<=n;j++){bestx[j]=x[j];}}return;}int *z=new int[maxl+1];for(int k=1;k<=maxl;k++){z[k]=y[k];}for(int j=x[i-1]+1;j<=r;j++){x[i]=j;Backtrack(i+1,r);for(int k=1;k<=maxl;k++){y[k]=z[k];}}delete[] z;
}int MaxStamp(int n,int m,int bestx[])
{Stamp X;int maxint=32767;int maxl=1500;X.n=n;X.m=m;X.maxvalue=0;X.maxint=maxint;X.maxl=maxl;X.bestx=bestx;X.x=new int [n+1];X.y=new int [maxl+1];for(int i=0;i<=n;i++){X.x[i]=0;}for(int i=1;i<=maxl;i++){X.y[i]=maxint;}X.x[1]=1;X.y[0]=0;X.Backtrack(2,1);delete[] X.x;delete [] X.y;return X.maxvalue;
}
/*
樣例:
郵票面值數:5
每張信封最多允許貼的郵票數:4
最大郵資:70
當前最優解:1 3 11 15 32
*/
7.電路板排列問題
將n塊電路板以最佳排列方式插入帶有n個插槽的機箱中,n塊電路板的不同排列方式對應不同的電路板插入方案。
設B={1, 2, …, n}是n塊電路板的集合,
L={N1, N2, …, Nm}是連接這n塊電路板中若干電路板的m個連接塊。
Ni是B的一個子集,且Ni中的電路板用同一條導線連接在一起。
x表示n塊電路板的一個排列,即在機箱的第i個插槽中插入的電路板編號是x[i]。
x所確定的電路板排列Density (x)密度定義為跨越相鄰電路板插槽的最大連線數。
例:
如圖,設n=8, m=5,
給定n塊電路板及其m個連接塊:
B={1, 2, 3, 4, 5, 6, 7, 8},N1={4, 5, 6},N2={2, 3},
N3={1, 3},N4={3, 6},N5={7, 8};
其中兩個可能的排列如圖所示,則該電路板排列的密度分別是2,3。
下1圖中,跨越插槽2和3,4和5,以及插槽5和6的連線數均為2。插槽6和7之間無跨越連線。其余插槽之間只有1條跨越連線。在設計機箱時,插槽一側的布線間隙由電路板的排列的密度確定。因此,電路板排列問題要求對于給定的電路板連接條件(連接塊),確定電路板的最佳排列,使其具有最小密度。
??
·算法設計:
(1)解空間:電路板排列問題是NP難問題,因此不大可能找到解此問題的多項式時間算法。考慮采用回溯法系統的搜索問題解空間的排列樹,找出電路板的最佳排列。
(2)設用數組B表示輸入,B[i][j]的值為1當且僅當電路板i在連接塊Nj中,total[j]是連接塊Nj中的電路板數,對于電路板的部分排列x[1:i],now[j]是x[1:i]中所包含的Nj中的電路板數,由此可知,連接塊Nj的連線跨越插槽i和i+1當且僅當now[j]>0且now[j]!=total[j],用這個條件來計算插槽i和i+1間的連線密度。
·算法實現:
#include <iostream>
#include <fstream>
#include <queue>
#include <algorithm>
using namespace std;const int MAX = 50;
int p[MAX][MAX];
int bestx[MAX];
int n, m; //電路板數,連接塊數class Node
{
public:int dep; //當前深度int cd; //當前排列長度int *x; //存儲當前排列x[1:dep]int *low; //電路塊中最左邊電路板int *high; //電路塊中最右邊電路板Node(){cd = 0;dep = 0;high = new int[m+1];low = new int[m+1];x = new int[n+1];}int len() //計算當前排列最小長度{int temp = 0;for(int j=1; j<=m; j++){if(low[j]<=n && high[j]>0 && temp<high[j]-low[j])temp = high[j] - low[j];}return temp;}
};int search()
{queue<Node> q;Node enode;int bestd = n + 1;int i, j;for(j=1; j<=m; j++){enode.high[j] = 0;enode.low[j] = n + 1;}for(i=1; i<=n; i++)enode.x[i] = i;while(true){if(enode.dep == n-1) //僅一個兒子結點,已經排完n-1個電路板,現在排最后一個{for(int j=1; j<=m; j++)if(p[ enode.x[n] ][j]>0 && n>enode.high[j])enode.high[j] = n;enode.cd = enode.len();if(enode.cd < bestd){bestd = enode.cd;copy(enode.x, enode.x+n+1, bestx);}}else{int cur = enode.dep + 1;for(i=enode.dep+1; i<=n; i++) //產生當前擴展結點的所有兒子結點{Node now;for(int j=1; j<=m; j++){now.low[j] = enode.low[j];now.high[j] = enode.high[j];if(p[ enode.x[i] ][j] > 0){if(cur < now.low[j])now.low[j] = cur;if(cur > now.high[j])now.high[j] = cur;}}now.cd = now.len();if(now.cd < bestd){now.dep = enode.dep + 1;copy(enode.x, enode.x+n+1, now.x);now.x[now.dep] = enode.x[i]; //相當于回溯中的swap(x[dep], x[i])now.x[i] = enode.x[now.dep];q.push(now);}}}if(q.empty())break;else{enode = q.front(); //下一層擴展結點q.pop();}}return bestd;
}int main()
{ifstream fin("input.txt");cout << "輸入電路板個數:";fin >> n;cout << n;cout << "\n輸入連接塊個數:";fin >> m;cout << m;cout << "\n輸入矩陣:\n";int i, j;for(i=1; i<=n; i++){for(j=1; j<=m; j++){fin >> p[i][j];cout << p[i][j] << " ";}cout << endl;}cout << "\n排列的最小長度為:" << search();cout << "\n最佳排列為:\n" ;for(i=1; i<=n; i++)cout << bestx[i] << " ";cout << endl;cout << endl;fin.close();return 0;
}
/*
樣例:
10 4
0 0 1 0 0
0 1 0 0 0
0 1 1 1 0
1 0 0 0 0
1 0 0 0 0
1 0 0 1 0
0 0 0 0 1
0 0 0 0 1
*/
8.n后問題
在n×n格的棋盤上放置彼此不受攻擊的n個皇后。按照國際象棋的規則,皇后可以攻擊與之處在同一行或同一列或同一斜線上的棋子。n后問題等價于在n×n格的棋盤上放置n個皇后,任何2個皇后不放在同一行或同一列或同一斜線上。
·算法設計:
(1)解向量:(x1, x2, … , xn)
(2)顯約束:xi=1,2, … ,n
(3)隱約束:
1)不同列:xi?xj
2)不處于同一正、反對角線:|i-j|?|xi-xj|
·算法分析:
*回溯法解N皇后問題
*使用一個一維數組表示皇后的位置
*其中數組的下標表示皇后所在的行
*數組元素的值表示皇后所在的列
*這樣設計的棋盤,所有皇后必定不在同一行
*
*假設前n-1行的皇后已經按照規則排列好
*那么可以使用回溯法逐個試出第n行皇后的合法位置
*所有皇后的初始位置都是第0列
*那么逐個嘗試就是從0試到N-1
*如果達到N,仍未找到合法位置
*那么就置當前行的皇后的位置為初始位置0
*然后回退一行,且該行的皇后的位置加1,繼續嘗試
*如果目前處于第0行,還要再回退,說明此問題已再無解
*
*如果當前行的皇后的位置還是在0到N-1的合法范圍內
*那么首先要判斷該行的皇后是否與前幾行的皇后互相沖突
*如果沖突,該行的皇后的位置加1,繼續嘗試
*如果不沖突,判斷下一行的皇后
*如果已經是最后一行,說明已經找到一個解,輸出這個解
*然后最后一行的皇后的位置加1,繼續嘗試下一個解
·算法實現:
#include<stdio.h>
#define MAX_LENGTH 1024
/** 檢查第n行的皇后與前n-1行的皇后是否有沖突* 發生沖突的充分必要條件是:* a) 兩皇后處于同一列,即a[i] == a[n]* b) 兩皇后處于同一斜線,即|a[i] - a[n]| == |i - n| == n - i*/
int k=1;
int is_conflict(int *a, int n)
{int flag = 0;int i;for ( i = 0; i < n; i++ ){if ( a[i] == a[n] || a[i] - a[n] == n - i || a[n] - a[i] == n - i ){flag = 1;break;}}return flag;
}
/** 輸出皇后的排列*/
void print_board(int *a, int n)
{int i, j;printf("第%d個排列為:\n",k++);for ( i = 0; i < n; i++ ){for ( j = 0; j < a[i]; j++ ){printf("○");}printf("●");for ( j = a[i] + 1; j < n; j++ ){printf("○");}printf("\n");}printf("------------------\n");
}
/** 初始化棋盤,所有皇后都在第0列*/
void init_board(int *a, int n)
{int i;for ( i = 0; i < n; i++ ){a[i] = 0;}
}
/** 解決N皇后問題*/
int queen(int n)
{int count = 0;int a[MAX_LENGTH];init_board(a, n);int i = 0;while ( 1 ){if ( a[i] < n ){// 如果皇后的位置尚未超出棋盤范圍// 需要檢查第i行的皇后是否與前i-1行的皇后沖突if ( is_conflict(a, i) ){// 如果沖突,嘗試下一個位置a[i]++;continue;}if ( i >= n - 1 ){// 如果已經到最后一行,也即找到一個解,首先輸出它count++;print_board(a, n);// 然后嘗試當前行的皇后的下一個位置a[n-1]++;continue;}// 沒有沖突,嘗試下一行i++;continue;}else{// 皇后的位置已經超出棋盤范圍// 那么該行的皇后復位a[i] = 0;// 回退到上一行i--;if ( i < 0 ){// 已經不能再退了,函數結束return count;}// 嘗試上一行的皇后的下個位置a[i]++;continue;}}
}
int main(void)
{int n = 8;int count = queen(n);printf("%d solutions in %d queens problem\n", count, n);return 0;
}
總結
以上是生活随笔為你收集整理的算法设计与分析第5章 回溯法(二)【回溯法应用】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 请问《邀请函》电影众筹是不是骗人的呢?
- 下一篇: 《OpenCV3编程入门》学习笔记6 图