TSP问题的解决与实现报告.docx
《TSP问题的解决与实现报告.docx》由会员分享,可在线阅读,更多相关《TSP问题的解决与实现报告.docx(32页珍藏版)》请在冰豆网上搜索。
TSP问题的解决与实现报告
1.问题描述
所谓TSP问题是指旅行家要旅行n个城市,要求各个城市经历且仅经历一次,并且要求所走的路程最短。
该问题又称为货郎担问题、邮递员问题、售货员问题,是图问题中最广为人知的问题。
2.基本要求
(1)上网查找TSP问题的应用实例;
(2)分析求TSP问题的全局最优解的时间复杂度;
(3)设计一个求近似解的算法;
(4)分析算法的时间复杂度。
3.提交报告
课程设计报告提交内容包括:
(1)问题描述;
(2)需求分析;(3)概要设计;(4)详细设计;(5)调试分析;(6)使用说明;(7)测试结果;(8)附录(带注释的源程序)。
参见“数据结构课程设计概述.pdf”和“数据结构课程设计示例.pdf”。
指导教师(签字):
系主任(签字):
批准日期:
2014年月日
1.问题描述
(1)题目要求
旅行家要旅行n个城市,要求各个城市经历且仅经历一次,最终要回到出发的城市,求出最短路径。
用图论的术语来说,假如有一个图G=(V,E),其中V是顶点集,E是边集,设D=(dij)是由顶点i和顶点j之间的距离所组成的距离矩阵。
TSP问题就是求出一条通过每个顶点且每个顶点只通过一次的具有最短距离的回路。
(2)基本要求
a.上网查找TSP问题的应用实例;
b.分析求TSP问题的全局最优解的时间复杂度;
c.设计一个求近似解的算法;
d.分析算法的时间复杂度。
(3)测试数据
5个城市的TSP问题:
注:
由于矩阵所表示的是两个城市之间的距离,所以该矩阵为对称矩阵
路程矩阵如图所示:
∞
25
41
32
28
25
∞
18
31
26
41
18
∞
7
1
32
31
7
∞
11
28
26
1
11
∞
最短路径为v0v1v4v2v3
2.需求分析
(1)本程序用于求解n个结点的最短哈密尔顿回路问题。
(2)程序运行后显示提示信息—“Pleaseinsertthenumberofcities:
”,例如用户输入5,则表示结点n的值为5;接下来程序输出提示信息—“Pleaseinsertthedistancebetweenonecityandanother:
”,例如用户输入测试数据中给出的路程矩阵,表示任意两个城市之间的距离,比如第一个城市到第0个城市之间的距离为25。
(3)用户输入数据完毕,程序将输出运算结果。
(4)测试数据均为正数,其中用999来表示两个城市之间距离为∞。
3.概要设计
为了实现上述程序功能,使用优先队列来维护结点表,因此需要图和队列两个抽象数据类型。
(1)图的抽象数据类型定义
ADTGraph{
Data:
具有相同类型的数据元素的集合,称为顶点集。
Relation:
顶点偶对的有穷集合。
Operation:
CreateGraph(&G,V,VR)
初始条件:
V是图中顶点集合,VR是图中顶点偶对集合。
操作条件:
按照V和VR的定义构造图G。
DestroyGraph(&G)
初始条件:
图G已经存在。
操作结果:
销毁G。
LocateVex(G,u)
初始条件:
图G已经存在,u和G中顶点有相同类型。
操作结果:
如果G中存在u,则返回u在G中的位置;否则返回相应信息。
GetVex(G,v)
初始条件:
图G已经存在,v是G中某个顶点。
操作结果:
返回v的值。
PutVex(&G,v,value)
初始条件:
图G已经存在,v是G中顶点。
操作结果:
对v赋值value。
FirstAdjvex(G,v)
初始条件:
图G已经存在,v是G中顶点。
操作结果:
返回v的第一个邻接顶点。
如果v在G中没有邻接顶点,则返回相应信息。
NextAdjvex(G,v,w)
初始条件:
图G已经存在,v是G中顶点,w是v的邻接顶点。
操作结果:
返回v的下一个邻接顶点。
如果w是v的最后一个邻接点,则返回相应信息。
InsertVex(&G,v,w)
初始条件:
图G已经存在,v和w是G中顶点。
操作结果:
在G中添加v。
DeleteVex(&G,v)
初始条件:
图G已经存在,v是G中顶点。
操作结果:
删除G中顶点v及其相关的边。
InsertEdge(&G,v,w)
初始条件:
图G已经存在,v和w是G中顶点。
操作结果:
在G中添加;如果G是无向图,则还增添。
DeleteEdge(&G,v,w)
初始条件:
图G已经存在,v和w是G中顶点。
操作结果:
在G中删除;如果G是无向图,则还删除。
DFSTraverse(G,v)
初始条件:
图G已经存在,v是G中顶点。
操作结果:
从v起深度访问G。
BFSTraverse(G,v)
初始条件:
图G已经存在,v是G中顶点。
操作结果:
从v起广度访问G。
}ADTGraph
(2)队列的抽象数据类型
ADTQueue{
Data:
具有相同数据类型的及先进先出特性的数据元素集合。
Relation:
相邻数据元素具有前驱和后继的关系。
Operation:
InitQueue(&Q)
初始条件:
无
操作结果:
创造一个空队列Q。
DestroyQueue(&Q)
初始条件:
队列Q已经存在。
操作结果:
销毁Q。
ClearQueue(&Q)
初始条件:
队列Q已经存在。
操作结果:
重置Q为空队列。
QueueLength(Q)
初始条件:
队列Q已经存在。
操作结果:
返回Q的元素个数。
GetHead(Q,&e)
初始条件:
队列Q已经存在并且非空。
操作结果:
用e返回Q的队头元素。
EnQueue(&Q,e)
初始条件:
队列Q已经存在。
操作结果:
重置Q为空队列。
DeQueue(&Q,&e)
初始条件:
队列Q已经存在且非空。
操作结果:
删除Q的队头元素,并用e返回其值。
}ADTQueue
(3)本程序包含三大模块:
主程序模块,TSP算法模块,辅函数模块。
三大模块之间的调用关系如下。
4.详细设计
(1)元素类型、结点类型和指针类型、变量和数据结构声明
Node*xnode;//父亲结点指针
Node*ynode;//儿子结点指针
Node*znode;//儿子结点指针
Node*qbase;//优先队列首指针
ElemTypebound;//当前可行解的最优值
typedefintElemType;//元素类型
#defineMAX_VALUE_OF_TYPE999;//代表∞
城市顶点用数字0,1,2,……,n-1编号。
在搜索的过程中,各个结点的数据是动态变化的,互不相同,发生回溯时,必须使用结点中原来的数据。
因此,每个结点的数据必须是局部与该结点的。
用如下的数据结构来定义结点中所使用的数据:
typedefstructNode{
ElemTypec[100][100];//路程矩阵
intinit_row[100];//路程矩阵的当前行映射为原始行
intinit_col[100];//路程矩阵的当前列映射为原始列
intcur_row[100];//路程矩阵的原始行映射为当前行
intcur_col[100];//路程矩阵的原始列映射为当前列
intad[100];//回路顶点邻接表
intk;//当前路程矩阵的阶
ElemTypew;//结点的下界
structNode*next;//队列链指针
}Node;
(2)队列类型
typedefstructQNode{
Node*data;//数据域
structQNode*next;//指针域
}QNode,*QueuePtr;
typedefstruct{
QueuePtrfront;//头指针,指向链队头结点
QueuePtrrear;//尾指针,指向链队列最后一个结点
}LinkQueue;
程序中所用到的关于优先队列基本操作实现的伪码算法如下:
voidInitQueue(LinkQueue&Q){
//构造一个空链队列Q
Q.front=Q.rear=newQNode;
Q.front->next=NULL;
}//InitQueue
voidEnQueue(LinkQueue&Q,Node*e){
//插入一个指针e到链队列Q中,成为新的队尾指针
QueuePtrp;
p=newQNode;
p->data=e;
p->next=NULL;
Q.rear->next=p;
Q.rear=p;
}//EnQueue
Node*DeQueue(LinkQueue&Q){
//若链队列Q为空,则返回NULL;否则返回指向数据的指针
QNode*p;
Node*e;
if(Q.front->next==NULL)returnNULL;
p=Q.front->next;
e=p->data;
Q.front->next=p->next;
if(Q.rear==p)Q.rear=Q.front;
deletep;
returne;
}//DeQueue
(3)辅助函数的实现(共7个)
ElemTypeRow_min(Node*node,introw,ElemType&second){
//计算路程矩阵行的最小值
if(node->c[row][0]c[row][1]){
temp=node->c[row][0];
second=node->c[row][1];
}
else{
temp=node->c[row][1];
second=node->c[row][0];
}
for(i=2;ik;i++){
if(node->c[row][i]second=temp;
temp=node->c[row][i];
}
elseif(node->c[row][i]second=node->c[row][i];
}
returntemp;
}
ElemTypeCol_min(Node*node,intcol,ElemType&second){
//计算路程矩阵列的最小值
if(node->c[0][col]c[1][col]){
temp=node->c[0][col];
second=node->c[1][col];
}
else{
temp=node->c[1][col];
second=node->c[0][col];
}
for(i=2;ik;i++){
if(node->c[i][col]second=temp;
temp=node->c[i][col];
}
elseif(node->c[i][col]second=node->c[i][col];
}
returntemp;
}
ElemTypeArray_red(Node*node){
//归约node所指向的结点的路程矩阵
sum=0;
for(i=0;ik;i++){//行归约
temp=Row_min(node,i,temp1);//行归约常数
for(j=0;jk;j++)
node->c[i][j]-=temp;
sum+=temp;//行归约常数累计
}
for(j=0;jk;j++){//列归约
temp=Col_min(node,j,temp1);//列归约常数
for(i=0;ik;i++)
node->c[i][j]-=temp;
sum+=temp;
}
returnsum;
}
ElemTypeEdge_sel(Node*node,int&vk,intv1){
//计算Dkl,选择搜索分支的边
d=0;
ElemType*row_value=newElemType[node->k];
ElemType*col_value=newElemType[node->k];
for(i=0;ik;i++)//每一行的次小值
Row_min(node,i,row_value[i]);
for(i=0;ik;i++)//每一列的次小值
Col_min(node,i,col_value[i]);
for(i=0;ik;i++){//对路程矩阵所有的0元素值
for(j=0;jk;j++){//计算相应的temp值
if(node->c[i][j]==0){
temp=row_value[i]+col_value[i];
if(temp>d){//求最大的temp值于d
d=temp;
vk=i;
v1=j;
}//保存相应的行、列号
}
}
}
deleterow_value;
deletecol_value;
returnd;
}
voidDel_rowcol(Node*node,intvk,intv1){
//删除路程矩阵的第vk行和第v1列的所有元素
for(i=vk;ik-1;i++)//元素上移
for(j=0;jnode->c[i][j]=node->c[i+1][j];
for(j=v1;jk-1;j++)//元素左移
for(i=0;inode->c[i][j]=node->c[i][j+1];
for(i=vk;ik-1;i++)//元素上移及左移
for(j=v1;jk-1;j++)
node->c[i][j]=node->c[i+1][j+1];
vk1=node->init_row[vk];//当前行转换为原始行vk1
node->row_cur[vk1]=-1;//原始行vk1置删除标志
for(i=vk1+1;inode->row_cur[i]--;
vl1=node->init_col[v1];//当前列v1转换为原始列vl1
node->col_cur[vl1]=-1;//原始列vl1置删除标志
for(i=vl1+1;inode->col_cur--;
for(i=vk;ik;i++)//修改vk及其后的当前行的对应原始行号
node->init_row[i]=node->init_row[i+1];
for(i=v1;ik;i++)//修改v1及其后的当前列的对应原始行号
node->init_col[i]=node->init_col[i+1];
node->k--;//当前矩阵的阶数减1
}
voidEdge_byp(Node*node,intvk,intvl){
//登记回路顶点邻接表,旁路有关的边
vk=init_row[vk];//当前行号转换为原始行号
vl=init_col[vl];//当前列号转换为原始列号
node->ad[vk]=vl;//登记回路顶点邻接表
k=node->row_cur[vl];//vl转换为当前行号k
l=node->col_cur[vk];//vk转换为当前列号l
if((k>=0)&&(l>=0))//当前行、列号均处于当前矩阵中
node->c[k][l]=MAX_VALUE_OF_TYPE;//旁路相应的边
}
Node*Initial(ElemTypec[][],intn){
//初始化
Node*node=newNode;//分配结点缓冲区
for(i=0;ifor(j=0;jnode->c[i][j]=c[i][j];
for(i=0;inode->init_row[i]=i;
node->init_col[i]=i;
node->row_cur=i;
node->col_cur=i;
}
for(i=0;inode->ad[i]=-1;
node->k=n;
returnnode;//返回结点指针
}
(4)TSP问题分支限界算法
voidTSP(ElemTypec[100][100],intn,int*ad){
bound=MAX_VALUE_OF_TYPE;
InitQueue(qbase);//初始化队列
xnode=Initial(c,n);//初始化父亲结点--------x结点
xnode->w=Array_red(xnode);//归约路程矩阵
while(xnode->k!
=0){
d=Edge_sel(xnode,vk,vl);//选择分支方向并计算Dkl
znode=newNode;//建立分支结点------z结点(右儿子结点)
*znode=*xnode;//x结点数据复制给z结点
znode->c[vk][vl]=MAX_VALUE_OF_TYPE;//旁路结点的边
Array_red(znode);//归约z结点路程矩阵
znode->w=xnode->w+d;//计算z结点的下界
if(znode->wEnQueue(qbase,znode);//将z结点插入优先队列
elsedeleteznode;//否则,剪去该结点
ynode=newNode;//建立分支节点----y结点(左儿子结点)
*ynode=*xnode;//x结点数据复制到y结点
Edge_byp(ynode,vk,vl);//登记回路邻接表,旁路有关的边
Del_rowcol(ynode,vk,vl);//删除结点y路程矩阵当前vk行vl列
ynode->w=Array_red(xnode);//归约y结点路程矩阵
ynode->w+=xnode->w;//计算y结点的下界
if(ynode->k==2){//路程矩阵只剩2阶
if((ynode->c[0][0]==0)&&(ynode->c[1][1]==0)){
ynode->ad[ynode->init_row[0]]=ynode->init_col[0];
ynode->ad[ynode->init_row[1]]=ynode->init_col[1];
}
else{
ynode->ad[ynode->init_row[0]]=ynode->init_col[1];
ynode->ad[ynode->init_row[1]]=ynode->init_col[0];
}//登记最后的两条边
ynode->k=0;
}
if(ynode->wEnQueue(qbase,ynode);//y结点插入优先队列
if(ynode->k==0)//更新当前可行解最优值
bound=ynode->w;
}
elsedeleteynode;//否则剪去y结点
xnode=DeQueue(qbase);//取优先队列首元素
}
w=xnode->w;//保存最短路线长度
for(i=0;iad[i]=xnode->ad[i];
deletexnode;//释放x结点缓冲区
while(qbase.front!
=qbase.rear){//释放队列节点缓冲区
xnode=DeQueue(qbase);
deletexnode;
}
cout<<"Thewayis:
";
for(intm=0;mcout<cout<cout<<"Thelengthofthewayis:
"<}
(5)主函数的伪码算法
voidmain(){
cout<<”Pleaseinsertthenumberofcities:
”;
cin>>n;
cout<<”Pleaseinsertthedistancebetweenonecityandanother:
”;
for(i=0;ifor(j=0;j{cin>>c[i][j];
cout<<””;}
cout<}
TSP(c,n,ad);
}//main
(6)函数调用关系图
5.调试分析
(1)
使用分支限界法求解的过程中,将动态地生成很多结点,用结点表来存放动态生成的结点信息。
因此必须按路程的下界来确定搜索的方向,因此可以用优先队列或堆来维护结点表。
在此使用优先队列来维护结点表。
(2)算法的时间复杂度分析
TSP算法的时间估计如下:
a.初始化父亲结点,归约父亲结点路程矩阵,都需要O(n2)时间。
b.while循环体的执行次数取决于所搜索的结点个数,假定所搜索的借点数为c。
在while循环内部,选择分支方向需要O(n2)时间。
c.把x结点数据复制到z结点(这里包括整个路程矩阵的复制工作),归约z结点的路程矩阵,都需要O(n2)时间。
d.把z结点插入到优先队列,在最坏的情况下需要O(c)时间。
e.把x结点复制到y结点,同样需要O(n2)时间。
f.登记回路邻接表,旁路有关的边,只需O
(1)时间。
g.删除y结点路程矩阵当前vk行vl列,归约y结点的路程矩阵,这些操作都需要O(n2)时间。
h.把y结点插入队列,删除队列首元素,都需要O(c)时间。
i.其余的花费为O
(1)时间。
j.因此,整个while循环在最坏的情况下需要O(cn2)时间。
k.最后,在算法的尾部,for循环保存路线的顶点邻接表于数组ad中作为算法的返回值,需要O(n)时间。
l.释放队列缓冲区的while循环在最坏情况下需要O(c)时间。
所以,整个算法的时间复杂度为O(cn2)。
(3)算法的空间复杂度分析
算法所需要的空间,主要花费在结点的存储空间。
每个结点需要O(n2)空间