问题描述:
旅行商问题(Traveling Salesman Problem,TSP)是旅行商要到若干个城市旅行,各城市之间的费用是已知的,为了节省费用,旅行商决定从所在城市出发,到每个城市旅行一次后返回初始城市,问他应选择什么样的路线才能使所走的总费用最短?此问题可描述如下:设G=(V,E)是一个具有边成本cij的有向图,cij的定义如下,对于所有的i和j,cij>0,若<i,j>不属于E,则cij=∞。令|V|=n,并假设n>1。 G的一条周游路线是包含V中每个结点的一个有向环,周游路线的成本是此路线上所有边的成本和。
问题分析:
旅行商问题要从图G的所有周游路线中求取最小成本的周游路线,而从初始点出发的周游路线一共有(n-1)!条,即等于除初始结点外的n-1个结点的排列数,因此旅行商问题是一个排列问题。排列问题比子集合的选择问题通常要难于求解得多,这是因为n个物体有n!种排列,只有 个子集合(n!>O( ))。通过枚举(n-1)!条周游路线,从中找出一条具有最小成本的周游路线的算法,其计算时间显然为O(n!)。
枚举法思想:程序中采用深度优先策略。(采用隐式和显式两种形式)
枚举算法的特点是算法简单,但运算量大,当问题的规模变大,循环的阶数越大,执行的速度越慢。如果枚举范围太大(一般以不超过两百万次为限),在时间上就难以承受。在解决旅行商问题时,以顶点1为起点和终点,然后求{2…N}的一个全排列,使路程1→{2…N}的一个全排列→1上所有边的权(代价)之和最小。所有可能解由(2,3,4,…,N)的不同排列决定。
核心代码(完整源代码见源代码)
为便于讨论,介绍一些关于解空间树结构的术语。在下面分析回溯法和分支限界法时都直接或间接用到解空间树。在解空间树中的每一个结点确定所求问题的一个问题状态(problem state)。由根结点到其它结点的所有路径则确定了这个问题的状态空间(state space)。解状态(solution states)表示一些问题状态S,对于这些问题状态,由根到S的那条路径确定了这解空间中的一个元组。答案状态(answer states)表示一些解状态S,对于这些解状态而言,由根到S的这条路径确定了这问题的一个解(即,它满足隐式约束条件)。解空间的树结构称为状态空间树(state space tree)。
对于旅行商问题,一旦设想出一种状态空间树,那么就可以先系统地生成问题状态,接着确定这些问题状态中的哪些状态是解状态,最后确定哪些解状态是答案状态,从而将问题解出。为了生成问题状态,采用两种根本不同的方法。如果已生成一个结点而它的所有儿子结点还没有全部生成,则这个结点叫做活结点。当前正在生成其儿子结点的活结点叫E-结点。不再进一步扩展或者其儿子结点已全部生成的生成结点就是死结点。在生成问题状态的两种方法中,都要用一张活结点表。在第一种方法中,当前的E-结点R一旦生成一个新的儿子C,这个儿子结点就变成一个新的E-结点,当完全检测了子树C之后,R结点就再次成为E-结点。这相当与问题状态的深度优先生成。在第二种状态生成方法中,一个E-结点一直保持到死结点为止。这两种方法中,将用限界函数去杀死还没有全部生成其儿子结点的那些活结点。如果旅行商问题要求找出全部解,则要生成所有的答案结点。使用限界函数的深度优先结点生成方法称为回溯法。E-结点一直保持到死为止的状态生成方法称为分支限界法。
回溯法思想:
为了应用回溯法,所要求的解必须能表示成一个n- 元组(x1,…,Xn),其中x1是取自某个有穷集Si。通常,所求解的问题需要求取一个使某一规范函数P(x1,…,Xn)取极大值(或取极小值或满足该规范函数条件)的向量。
假定集合Si的大小是mi,于是就有m=m1m2…Mn个n-元组可能满足函数P。所谓硬性处理是构造这m个n-元组并逐一测试它们是否满足P,从而找出该问题的所有最优解。而回溯法的基本思想是,不断地用修改过的函数Pi(x1,…Xi)(即限界函数)去测试正在构造中的n-元组的部分向量(x1,…,Xi),看其是否可能导致最优解。如果判定(x1,…,Xi)不可能导致最优解,那么就可能要测试的后n-i个元素组成的向量一概略去。因此回溯法作的次数比硬性处理作的测试次数(m次)要少得多。用回溯法求解的旅行商问题,即在枚举法的基础上多了一个约束条件,约束条件可以分为两种类型:显示约束和隐式约束。
核心代码(完整源代码见源代码)
分支限界法思想:本题采用FIFO分支限界法。
如前所述,分支限界法是在生成当前E-结点全部儿子之后再生成其它活结点的儿子,且用限界函数帮助避免生成不包含答案结点子树的状态空间的检索方法。在总的原则下,根据对状态控件树中结点检索的次序的不同又将分支限界设计策路分为数种不同的检索方法。在求解旅行商问题时,程序中采用FIFO检索(First In First Out),它的活结点表采用一张先进先出表(即队列)。可以看出,分支限界法在两个方面加速了算法的搜索速度,一是选择要扩展的节点时,总是选择选择一个最小成本的结点,尽可能早的进入最有可能成为最优解的分支;二是扩展节点的过程中,舍弃导致不可行解或导致非最优解的子结点。
核心代码(完整源代码见源代码)
贪心法思想:
贪心法是一种改进了的分级处理方法。它首先旅行商问题描述,选取一种度量标准。然后按这种度量标准对n个输入城市排序,并按序一次输入一个城市。如果这个输入和当前已构成在这种量度意义下的部分最优解加在一起不能产生一个可行解,则不把这个城市加入到这部分解中。这种能够得到某种量度意义下的最优解的分级处理方法成为谈心方法。
获得最优路径的贪心法应一条边一条边地构造这棵树。根据某种量度来选择将要计入的下一条边。最简单的量度标准是选择使得迄今为止计入的那些边的成本的和有最小增量的那条边。
核心代码(完整源代码见源代码)
源代码:
在程序执行目录下建立data.txt文件,用于存放城市节点信息,格式如下:
5 5
0 7 6 1 3
7 0 3 7 8
6 3 0 12 11
1 7 12 0 2
3 8 11 2 0
第一行表示为5行5列,之后为各个节点的权值;
程序执行前先建立如下头文件,用于存储和表示节点信息:
//------------------------------------- AdjtwGraph.h文件-------------------------------------------------- #ifndef AdjTWGraph_H #define AdjTWGraph_H #include <vector> #include <iostream> using namespace std; const int MaxV=100; struct Edge { int dest; int weight; Edge * next; Edge(){} Edge(int d,int w):dest(d),weight(w),next(NULL){} }; struct item { int data; Edge * adj; }; class AdjTWGraph { private: item vertices[MaxV]; int numV,numE; public : AdjTWGraph(); ~AdjTWGraph(); int NumV(){return numV;} int NumE(){return numE;} int GetValue(const int i); int GetWeight(const int v1,const int v2); void InsertV(const int & vertex); void InsertE(const int v1,const int v2,int weight); friend ostream& operator<<(ostream& os, AdjTWGraph & m) { for (int i = 0; i < m.numV ; i++) { for (int j = 0; j < m.numV; j++) os << right << m.GetWeight(i,j) << " "; os << endl; } return os; } friend istream& operator>>(istream& is, AdjTWGraph & m) { int t; for (int i = 0; i < m.NumV(); i++) for (int j = 0; j < m.NumV(); j++) { is >> t; m.InsertE(i,j,t); } return is; } }; AdjTWGraph::AdjTWGraph() { for(int i=0;i<MaxV;i++) vertices[i].adj=NULL; numV=0;numE=0; } AdjTWGraph::~AdjTWGraph() { for(int i=0;i<numV;i++) { Edge * p=vertices[i].adj,*q; while(p!=NULL) { q=p->next;delete p;p=q; } } } int AdjTWGraph::GetValue(const int i){ return vertices[i].data; } int AdjTWGraph::GetWeight(const int v1,const int v2) { Edge *p=vertices[v1].adj; while(p!=NULL && p->dest<v2) p=p->next; if(v2!=p->dest) { return 0; } return p->weight; } void AdjTWGraph::InsertV(const int & v) { vertices[numV].data=v; numV++; } void AdjTWGraph::InsertE(const int v1,const int v2,int weight) { Edge * q=new Edge(v2,weight); if(vertices[v1].adj==NULL) vertices[v1].adj=q; else { Edge *curr=vertices[v1].adj,*pre=NULL; while(curr!=NULL && curr->dest<v2) { pre=curr;curr=curr->next; } if(pre==NULL){ q->next=vertices[v1].adj;vertices[v1].adj=q; } else { q->next=pre->next;pre->next=q; } } numE++; } #endif //------------------------------------- tsp.cpp文件-------------------------------------------------- #include "AdjtwGraph.h" #include <fstream> #include <vector> #include <algorithm> #include <ctime> #include <queue> using namespace std; ofstream fout("out.txt"); int N; AdjTWGraph g; struct Node { int currentIndex; int level; Node * previous; Node(int L = 0, int V = 0, Node *p = NULL):level(L),currentIndex(V), previous(p) {} }; class TspBase { protected: vector<int> currentPath; vector<int> bestPath; int cv; int bestV; Node * root; int SumV(); void EnumImplicit(int k); void BackTrackImplicit(int k); void EnumExplicit(Node * r); void BackTrackExplicit(Node * r); void FIFOBB(); bool Valid(Node *p,int v) // { bool flag = true; for(Node *r = p; r->level > 0 && V; r = r->previous) flag = r->currentIndex !=v; return flag; } void StoreX(Node * p) // {for(Node *r = p; r->level >0 ; r = r->previous ) { currentPath[r->level-1] = r->currentIndex; } } void Print(); public: TspBase(){currentPath.resize(N); bestPath.resize(N); } ~TspBase(){currentPath.resize(0);bestPath.resize(0);} void TspEnumImplicit(); void TspBackTrackImplicit(); void TspEnumExplicit(); void TspBackTrackExplicit(); void TspBB(); void TspGreedy(); void DataClear(bool flag) { currentPath.resize(N); bestPath.resize(N); if(flag) { Node * p=root,*q; while(p!=NULL) {q=p->previous; delete p; p=q;} } } }; void TspBase::TspEnumImplicit() // 枚举隐式 { fout<<"TspEnumImplicit ..."<<endl; cv=0; bestV=10000; for(int i=0;i<N;i++) currentPath[i]=i; EnumImplicit(1); Print(); } void TspBase::EnumImplicit(int k) { if(k == N) { if((cv + g.GetWeight(currentPath[N-1],0)) < bestV) { bestV = cv + g.GetWeight(currentPath[N-1],0); for(int i = 0; i < N; i++) bestPath[i] = currentPath[i]; } } else for(int j = k; j < N; j++) { swap(currentPath[k],currentPath[j]); cv += g.GetWeight(currentPath[k-1],currentPath[k]); EnumImplicit(k+1); cv -= g.GetWeight(currentPath[k-1],currentPath[k]); swap(currentPath[k],currentPath[j]); } } void TspBase::TspEnumExplicit() // 枚举显式 { fout<<"TspEnumExplicit ..."<<endl; cv=0; bestV=10000; for(int i=0;i<N;i++) currentPath[i]=i; root=new Node(0,-1,NULL); EnumExplicit(root); Print(); } void TspBase::EnumExplicit(Node * r) { if(r->level == N) { StoreX(r); cv = SumV(); if(cv < bestV) { bestV = cv ; for(int i = 0; i < N; i++) bestPath[i] = currentPath[i]; } } else for(int i = 0; i < N; i ++) { if(Valid(r,i)) { Node *q = new Node(r->level+1,i,r); EnumExplicit(q); } } } void TspBase::TspBackTrackImplicit() //回溯隐式 { fout<<"TspBackTrackImplicit ..."<<endl; cv=0; bestV=10000; for(int i=0;i<N;i++) currentPath[i]=i; BackTrackImplicit(1); Print(); } void TspBase::BackTrackImplicit(int k) { if(k == N) { if((cv + g.GetWeight(currentPath[N-1],0)) < bestV) { bestV = cv + g.GetWeight(currentPath[N-1],0); for(int i = 0; i < N; i++) bestPath[i] = currentPath[i]; } } else for(int j = k; j < N; j++) { if((cv + g.GetWeight(currentPath[k-1],currentPath[j])) < bestV) { swap(currentPath[k],currentPath[j]); cv += g.GetWeight(currentPath[k-1],currentPath[k]); BackTrackImplicit(k+1); cv -= g.GetWeight(currentPath[k-1],currentPath[k]); swap(currentPath[k],currentPath[j]); } } } void TspBase::TspBackTrackExplicit() // 回溯显式 { fout<<"TspBackTrackExplicit ..."<<endl; cv=0; bestV=10000; for(int i=0;i<N;i++) currentPath[i]=i; root=new Node(0,-1,NULL); BackTrackExplicit(root); Print(); } void TspBase::BackTrackExplicit(Node * r) { int w=0; //初值 if(r->level == N) { StoreX(r); cv = SumV(); if(cv < bestV) { bestV = cv ; for(int i = 0; i < N; i++) bestPath[i] = currentPath[i]; } } else for(int i = 0; i < N; i ++) { if(Valid(r,i)) { Node *q = new Node(r->level+1,i,r); w += g.GetWeight(q->currentIndex,i); if(w < bestV) BackTrackExplicit(q); w -= g.GetWeight(q->currentIndex,i); } } } void TspBase::Print() // { fout<<"the shortest path is "; for(unsigned i = 0; i < N; i++) fout<<bestPath[i] + 1<<"--"; fout<<"1"<<endl; fout<<"minimum distance is "<<bestV<<endl; } void TspBase::TspBB() // 分支限界法 { fout<<"TspBB(FIFOBB) ........"<<endl; cv = 0; bestV = 100000; for(unsigned i = 0; i < N; i++) currentPath[i] = i; root=new Node(0,-1,NULL); FIFOBB(); Print(); } void TspBase::FIFOBB() { queue<Node*> q; Node *r; q.push(root); int w=0; //初值 while(!q.empty()) { r = q.front(); q.pop(); if(r->level == N) { StoreX(r); cv = SumV(); if(cv < bestV) { bestV = cv ; for(int i = 0; i < N; i++) bestPath[i] = currentPath[i]; } } else for(int i = 0; i < N; i ++) { if(Valid(r,i)) { Node *s = new Node(r->level+1,i,r); w += g.GetWeight(s->currentIndex,i); if(w < bestV) q.push(s); w -= g.GetWeight(s->currentIndex,i); } } } } int TspBase::SumV() //用于FIFOBB { int s = 0; for(int i = 0; i < N; i++) s += g.GetWeight(currentPath[i],currentPath[(i + 1)%N]); return s; } void TspBase::TspGreedy() //TSP贪心算法 { fout<<"TspGreedy ........"<<endl; bestV = 0; vector<int> NEAR(N); // NEAR[0] = -1; for (int i = 1; i < N; i++) NEAR[i] = 0; bestPath[0] = 1; int t; for (int s = 1; s < N; s++) { int j = 1; while (j < N && NEAR[j] < 0) / j++; int K = j; for (int k = j + 1; k < N; k++) if (NEAR[k] >= 0 && g.GetWeight(k,NEAR[k]) < g.GetWeight(j,NEAR[j])) j = k; bestPath[s] = j + 1; bestV +=g.GetWeight(j,NEAR[j]); NEAR[j] = -1; for (k = K; k < N; k++) //调整NEAR值 if (NEAR[k] >= 0) NEAR[k] = j; t = j; } bestV += g.GetWeight(t,0); fout<<"the shortest path is "; for(unsigned w = 0; w < N; w++) fout<<bestPath[w] <<"--"; fout<<"1"<<endl; fout<<"minimum distance is "<<bestV<<endl; } int main(int argc, char* argv[]) { int m,n; ifstream fin("data.txt"); if(fin.bad()) return 1; fin >> m >> n; N = n; for(int i=0;i<N;i++) g.InsertV(i); fin >> g; TspBase it; it.TspEnumImplicit(); it.DataClear(false); it.TspBackTrackImplicit(); it.DataClear(false); it.TspEnumExplicit(); it.DataClear(true); it.TspBackTrackExplicit(); it.DataClear(true); it.TspBB(); it.DataClear(true); it.TspGreedy(); it.DataClear(false); return 0; }
执行结果:
the shortest path is 1--3--2--5--4--1
minimum distance is 20