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

瘦欲@ 提交于 2020-01-30 00:40:04

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(n2×2n)\mathrm{O}\left(n^{2} \times 2^{n}\right)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!