回溯法、分支限界法解决旅行商TSP问题

TSP问题描述

旅行商从驻地出发,经过每个需要访问的城市一次且只有一次,并最终返回出发点。如何安排路线,使旅行总路程最短?即求解最短哈密顿回路。
在这里插入图片描述

回溯法解tsp问题(深度优先)

以深度优先的方式,从根节点开始,依次扩展树节点,直到达到叶节点——搜索过程中动态产生解空间
在这里插入图片描述

代码

#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define NUM 42
#define NoEdge 99999
struct  MinHeapNode{//解空间树的节点
	double lb,//子树费用的下界 
	cc;//当前费用 
	int s,
	*x;
	struct MinHeapNode *next;
};
int bestx[NUM+1];//最优路径
int x[NUM+1];
double cw=0.0,bestw=99999.0;//cw是当前路径值,bestw是最优路径值
double **w;//距离矩阵
int count=0;//计算节点数
MinHeapNode* head = 0; /*堆头*/
MinHeapNode* fq = 0; /*堆第一个元素*/
MinHeapNode* lq = 0; /*堆最后一个元素*/
void swap(int i,int j){//用于交换
	int temp=x[i];
	x[i]=x[j];
	x[j]=temp;
}

void backtrackTSP(int i,int n){//回溯法求解
	count++; 
	if(i==n){//到达叶节点
		if(w[x[n-1]][x[n]]!=99999&&w[x[n]][x[1]]!=99999){//叶节点的父节点和叶节点是否有路径,叶节点和第一个节点是否有路径
			if(cw+w[x[n-1]][x[n]]+w[x[n]][x[1]]<bestw){//当前路径值是否会优于当前最优值
				bestw=cw+w[x[n-1]][x[n]]+w[x[n]][x[1]];
				for(int j=1;j<=n;j++)
					bestx[j]=x[j];
			}
		}
	}
	else{//非叶节点
		for(int j=i;j<=n;j++){
			if(w[x[i-1]][x[j]]!=99999&&cw+w[x[i-1]][x[j]]<bestw){//若当前节点到下一节点无路径或当前路径值已经大于当前最优值,进行剪枝
				swap(i,j);
				cw=cw+w[x[i-1]][x[i]];
				backtrackTSP(i+1,n);
				//回溯失败 
				cw=cw-w[x[i-1]][x[i]];
				swap(i,j);
			}
		}
	}
}

int main(){
	FILE* fp1=fopen("22基站.txt","r");
	FILE* fp2=fopen("15基站.txt","r");
	FILE* fp3=fopen("20基站.txt","r");
	FILE* fp4=fopen("30基站.txt","r");
	w=(double**)malloc(sizeof(double*)*(NUM+1));
	for(int i=0;i<=NUM;i++)
		w[i]=(double*)malloc(sizeof(double)*(NUM+1));
	
	for(int i=1;i<=22;i++)//读入22基站的连接矩阵 
		for(int j=1;j<=22;j++)
			fscanf(fp1,"%lf",&w[i][j]);
	for(int i=1;i<=15;i++)//读入15个基站的初始顺序 
		fscanf(fp2,"%d",&x[i]);
	backtrackTSP(2,15);
	printf("%lf\n",bestw);
	for(int i=1;i<=15;i++)
		printf("%d ",bestx[i]);
	printf("\n%d\n",count);
	count=0;
	
	cw=0.0;
	bestw=99999.0;	
	for(int i=1;i<=20;i++)//读入20个基站的初始顺序 
		fscanf(fp3,"%d",&x[i]);
	backtrackTSP(2,20);
	printf("%lf\n",bestw);
	for(int i=1;i<=20;i++)
		printf("%d ",bestx[i]);
	printf("\n%d\n",count);
}

基站数据

在这里插入图片描述

运行结果

在这里插入图片描述

分支限界法解tsp问题(广度优先)

在这里插入图片描述

代码

#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define NUM 42
#define NoEdge 99999
struct  MinHeapNode{
	double lb,//子树费用的下界 
	cc;//当前费用 
	int s,
	*x;
	struct MinHeapNode *next;
};
int bestx[NUM+1];
int x[NUM+1];
double cw=0.0,bestw=99999.0;
double **w;
double upcost=0.0;//问题的上界 
MinHeapNode* head = 0; /*堆头*/
MinHeapNode* fq = 0; /*堆第一个元素*/
MinHeapNode* lq = 0; /*堆最后一个元素*/

void swap(int i,int j){
	int temp=x[i];
	x[i]=x[j];
	x[j]=temp;
}

int DeleteMin(MinHeapNode*&E)//从堆中取出下一个节点
{
	MinHeapNode* tmp = NULL;
	tmp = fq;
	// w = fq->weight ;
	E = fq;
	if(E == NULL)
		return 0;
	head->next = fq->next; /*一定不能丢了链表头*/
	fq = fq->next;
 	//free(tmp) ;
	return 0;
}

int Insert(MinHeapNode* hn)//在堆中插入节点
{
	if(head->next == NULL)
	{
		head->next = hn; //将元素放入链表中
		fq = lq = head->next; //一定要使元素放到链中
	}else
	{
		MinHeapNode *tmp = NULL;
		tmp = fq;
		if(tmp->lb > hn->lb)
		{
			hn->next = tmp;
			head->next = hn;
			fq = head->next; 
		}else
		{
			for(; tmp != NULL;)
			{
				if(tmp->next != NULL && tmp->next->lb > hn->lb)
				{
					hn->next = tmp->next;
					tmp->next = hn;
					break;
				}
				tmp = tmp->next;
			}
		}
		if(tmp == NULL)
		{
			lq->next = hn;
			lq = lq->next;
		}
	}
	return 0;
}

void up_cost(int k,int n){//用于求解问题上界 
	//贪心法+回溯法求解问题上界
	//printf("beging:%d\n",k);
	if(k==n){
		if(w[x[k]][x[1]]!=NoEdge){
			flag=1;
			upcost=cw+w[x[k]][x[1]];
		}
	}
	else{
		for(int i=0;i<n-k-1;i++){//冒泡排序 
			for(int j=k+1;j<n-i;j++){
				if(w[x[k]][x[j]]>w[x[k]][x[j+1]])
					swap(j,j+1);
			}
		}
		for(int i=k+1;i<=n;i++){
			if(flag==1)
				break;
			if(w[x[k]][x[i]]!=NoEdge){
				//printf("%d,%d,%lf\n",x[k],x[i],w[x[k]][x[i]]);
				cw+=w[x[k]][x[i]];
				swap(k+1,i);
				up_cost(k+1,n);
				cw-=w[x[k]][x[k+1]];//失败回溯 
				swap(k+1,i);
			}		
		}
	}
}

void low_cost(MinHeapNode *p,int n){//用于求解问题下界 
	p->lb=0.0;
	p->lb+=2*p->cc;//已经经过的路径总长的两倍
	double min=NoEdge;
	int min1;
	/*//从当前已经走过的第一个城市出发,走向最近的1个未遍历城市的距离和
	for(int j=p->s+1;j<n;j++){
		if(w[p->x[0]][p->x[j]]<min)
			min=w[p->x[0]][p->x[j]];
	}
	p->lb+=min;
	min=NoEdge;*/
	for(int i=p->s+1;i<n;i++){//进入/离开未遍历城市时,各未遍历城市带来的最小路径成本
		for(int j=0;j<n;j++){
			if(w[p->x[i]][p->x[j]]<min){
				min=w[p->x[i]][p->x[j]];
				min1=j;
			}		
		}
		p->lb+=min;
		min=NoEdge;
		for(int j=0;j<n;j++){
			if(w[p->x[i]][p->x[j]]<min&&j!=min1)
				min=w[p->x[i]][p->x[j]];
		}
		p->lb+=min;
		min=NoEdge;
	}
	p->lb/=2;
	
	if(p->s==n-1)
		p->lb+=w[p->x[n-1]][p->x[0]];
	else{
		//从当前已经走过的第一个城市出发,走向最近的1个未遍历城市的距离和
		for(int j=p->s+1;j<n;j++){
			if(w[p->x[0]][p->x[j]]<min)
				min=w[p->x[0]][p->x[j]];
		}
		p->lb+=min;
		min=NoEdge;
	}
	
	/*for(int i=0;i<n;i++)
		printf("%d ",p->x[i]);
	printf("\n%lf\n",p->lb);*/
}

void BBTSP(int v[],int n)//回溯法求解
{//解旅行售货员问题的优先队列式分支限界法
	/*初始化最优队列的头结点*/
	head = (MinHeapNode*)malloc(sizeof(MinHeapNode));
	head->cc = 0;
	head->x = 0;
	head->lb = 0;
	head->next = NULL;
	head->s = 0;

	MinHeapNode *E ;
	E = (MinHeapNode*)malloc(sizeof(MinHeapNode));
	E->x = new int[n];
	for(int i = 0; i <n; i++)
		E->x[i] = x[i+1];
		
	E->s = 0;
	low_cost(E,n);
	E->cc = 0;
	E->next = NULL; //初始化当前扩展节点
	//搜索排列空间树
	while(E->s >=0)
	{//非叶结点
		//printf("s:%d\n",E->s);
		//printf("chuli:%d\n",E->x[E->s]);
		//printlen();
		//printf("-------s:%d---------\n",E->s);
		if(E->s == n - 1)//到达叶节点 
		{
			/*
			如果该叶结点的路径值和所有节点的lb相比是最小的,输入最优解算法结束
			否则更新上界 
			*/
			/*printf("!!!!!!\n");
			for(int k=0;k<n;k++)
				printf("%d ",E->x[k]);
			printf("\n");*/
			if(w[E->x[E->s]][E->x[0]]==NoEdge){//无法形成回路
				//printf("11111\n"); 
				DeleteMin(E);
				continue;
			}
			else{
				E->cc+=w[E->x[E->s]][E->x[0]];
				bestw=E->cc;
				for(int i=0;i<n;i++)
					bestx[i+1]=E->x[i];
				break;
			}
				
		}else
		{/*扩展子节点,将可行子节点加入优先队列中*/
			//printf("2,upcost:%lf\n",upcost);
			for(int i = E->s + 1; i < n; i++)
				if(w[E->x[E->s]][E->x[i]] != NoEdge)
				{ /*当前扩展节点到其他节点有边存在*/
					//可行儿子结点
					//printf("3\n");
					double cc = E->cc + w[E->x[E->s]][E->x[i]]; /*加上节点i后当前节点路径*/
					MinHeapNode* N;
					N = (MinHeapNode*)malloc(sizeof(MinHeapNode));
					N->x = new int[n];
					for(int j = 0; j < n; j++)
						N->x[j] = E->x[j];
					N->x[E->s + 1] = E->x[i];
					N->x[i] = E->x[E->s + 1];
					N->cc = cc; /*更新当前路径距离*/
					N->s = E->s + 1; /*更新当前节点*/
					low_cost(N,n);
					//printf("cc:%lf\n",N->cc);
					//printf("lb:%lf\n",N->lb);
					if(N->lb<=upcost)
					{//子树可能含最优解,结点插入最小堆
						/*添加当前路径*/
						//printf("add:%d\n",N->x[E->s+1]);
						//printf("4\n");
						N->next = NULL;
						Insert(N); /*将这个可行儿子结点插入到活结点优先队列中*/
					}
				}
			free(E);
		}//完成结点扩展
		DeleteMin(E);//取下一扩展结点
		if(E == NULL)
			break; //堆已空
	}

	if(bestw == NoEdge)
		return ;//无回路
	while(true)
	{//释放最小堆中所有结点
		free(E->x);
		DeleteMin(E);
		if(E == NULL)
			break;
	}
}

int main(){
	FILE* fp1=fopen("22基站.txt","r");
	FILE* fp2=fopen("15基站.txt","r");
	FILE* fp3=fopen("20基站.txt","r");
	FILE* fp4=fopen("30基站.txt","r");
	w=(double**)malloc(sizeof(double*)*(NUM+1));
	for(int i=0;i<=NUM;i++)
		w[i]=(double*)malloc(sizeof(double)*(NUM+1));
	
	for(int i=1;i<=22;i++)//读入22基站的连接矩阵 
		for(int j=1;j<=22;j++)
			fscanf(fp1,"%lf",&w[i][j]);
			
	for(int i=1;i<=15;i++)//读入15个基站的初始顺序 
		fscanf(fp2,"%d",&x[i]);

	
	up_cost(1,15);//求问题上界 
	cw=0.0;
	printf("upcost:%lf\n",upcost); 
	BBTSP(bestx,15);
	//backtrackTSP(2,15);
	printf("最优值:%lf\n",bestw);
	printf("最优路径:"); 
	for(int i=1;i<=15;i++)
		printf("%d ",bestx[i]);
	
	cw=0.0;
	bestw=99999.0;
	for(int i=1;i<=20;i++)//读入20个基站的初始顺序 
		fscanf(fp3,"%d",&x[i]);
	up_cost(1,20);//求问题上界
	cw=0.0;
	printf("\nupcost:%lf\n",upcost);
	BBTSP(bestx,20);
	printf("最优值:%lf\n",bestw);
	printf("最优路径:"); 
	for(int i=1;i<=20;i++)
		printf("%d ",bestx[i]);

}

运行结果

在这里插入图片描述

结果分析

明显可以看出分支限界法求解所用的时间是少于回溯法的,毕竟前者设计上便是以空间换时间。回溯法的时间复杂度是 O ( n ! ) O(n!) O(n!),而分支限界法的时间复杂度是 O ( n 2 × 2 n ) \mathrm{O}\left(n^{2} \times 2^{n}\right) O(n2×2n)

  • 11
    点赞
  • 152
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
支限界法类又称为剪枝限界法或分支定界法,它类似于回溯法,也是一种在问题的解空间树T上搜索问题解的算法。它与回溯法有两点不同:①回溯法只通过约束条件剪去非可行解,而分支限界法不仅通过约束条件,而且通过目标函数的限界来减少无效搜索,也就是剪掉了某些不包含最优解的可行解。②在解空间树上的搜索方式也不相同。回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。分支限界法的搜索策略是:在扩展结点处,先生成其所有的儿子结点(分支),然后再从当前的活结点表中选择下一个扩展结点。为了有效地选择下一扩展结点,以加速搜索的进程, 在每一活结点处,计算一个函数值(限界),并根据这些已计算出的函数值,从当前活结点表中选择一个最有利的结点作为扩展结点,使搜索朝着解空间树上有最优解的分支推进,以便尽快地找出一个最优解。 从活结点表中选择下一扩展结点的不同方式导致不同的分支限界法。最常见的有以下两种方式: ①队列式(FIFO)分支限界法:队列式分支限界法将活结点表组织成一个队列,并按队列的先进先出原则选取下一个结点为当前扩展结点。 ②优先队列式分支限界法:优先队列式分支限界法将活结点表按照某个估值函数C(x)的值组织成一个优先队列,并按优先队列中规定的结点优先级选取优先级最高的下一个结点成为当前扩展结点。 影响分支限界法搜索效率的有两个主要因素:一是优先队列Q的优先级由C(x)确定,它能否保证在尽可能早的情况下找到最优解,如果一开始找到的就是最优解,那么搜索的空间就能降低到最小。二是限界函数u(x),它越严格就越可能多地剪去分支,从而减少搜索空间。 在用分支限界法解决TSP问题时,有不少很好的限界函数和估值函数已经构造出来出了(限于篇幅,这里不做详细介绍), 使得分支限界法在大多数情况下的搜索效率大大高于回溯法。但是,在最坏情况下,该算法的时间复杂度仍然是O(n!),而且有可能所有的(n-1)!个结点都要存储在队列中。 近似算法是指不能肯定找到最优解的算法,但通常找到的也是比较好的解,或称近似最优解。[20]一般而言,近似算法的时间复杂度较低,通常都是多项式时间内的。由于近似算法的时间效率高,所以在实际应用中,主要是使用近似算法,这一类算法也一直是研究的主要对象。传统的近似算法以采用贪心策略和局部搜索为主,而几十年来,随着以遗传算法为代表的新型启发式搜索算法的逐步完善,在解决TSP问题上获得了巨大的成功。遗传算法、模拟退火算法、蚁群算法等已经成为公认的好算法。在本节中,将介绍传统的近似算法
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值