HNU_算法_实验4(2021级)-经典案例2-分支限界法求解TSP问题

  一、实验名称

分支限界法求解TSP问题;

        问题描述:某售货员要到若干城市去推销商品,已知各城市之间的路程,他要选定一条从驻地出发,经过每个城市一遍,最后回到住地的路线,使总的路程最短。

二、实验目的

        通过上机实验,要求掌握分支限界法求解TSP问题的问题描述、算法设计思想、程序设计。

三、实验原理

        利用分支限界法求解TSP问题,并计算出程序运行所需要的时间。

四、实验步骤

       算法描述:

        解旅行售货员问题的优先队列式分支限界法用优先队列存储活结点表。

活结点m在优先队列中的优先级定义为:活结点m对应的子树费用下界lcost。

        lcost=cc+rcost,其中,cc为当前结点费用,rcost为当前顶点最小出边费用加上剩余所有顶点的最小出边费用和。

        优先队列中优先级最大的活结点成为下一个扩展结点

        排列树中叶结点所相应的载重量与其优先级(下界值)相同,即:叶结点所相应的回路的费用(bestc)等于子树费用下界lcost的值。

       

        

五、关键代码

1.排列树的节点定义

struct node
{
    int cl;//当前走过的路径长度
    int id;//处理的第几个城市
    int x[1000];//记录当前路径,下标从1开始

    node() {}
    node(int cl_,int id_)
    {
        cl=cl_;
        id=id_;
        memset(x,0,sizeof(x));
    }
};

2.用于构建最小堆(优先队列)

struct cl_cmp {
    //当前路径长度短的优先级高
    bool operator()(node n1, node n2)
    {
        return n1.cl > n2.cl;
    }
};

void bfs(int n,int &bestl,vector<int> &bestx,vector<vector<int> > &m)
{
    //选用最小堆
    priority_queue<node,vector<node>,cl_cmp> q;
    //创建一个节点,从该节点开始,因为1是固定位,其实是从1开始探索
    node temp(0,2);
    //初始化解向量
    for(int i=1; i<=n; ++i)
        temp.x[i]=i;
    q.push(temp);
    node cur;//当前节点,也就是活节点
    int t;
    while(!q.empty())
    {
        cur=q.top();
        q.pop();
        t=cur.id;
        if(t==n)
        {
            //满足约束条件,有路径
            //检测图G是否存在一条从顶点x[n-1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边
            if(m[cur.x[t-1]][cur.x[t]]!=INF&&m[cur.x[t]][1]!=INF)
            {
                if(cur.cl+m[cur.x[t-1]][cur.x[t]]+m[cur.x[t]][1]<bestl)
                {
                    //更新最优解和最优路径
                    bestl=cur.cl+m[cur.x[t-1]][cur.x[t]]+m[cur.x[t]][1];
                    for(int i=1; i<=n; ++i)
                        bestx[i]=cur.x[i];
                }
            }continue;
        }
        //大于等于最优路径,没必要继续探索了,从下一个节点开始
        if(cur.cl>=bestl)
            continue;
        //从当前节点开始探索t-1 -> t,t+1,t+2...
        for(int j=t; j<=n; ++j)
        {
            //满足约束条件和限界条件
            if(m[cur.x[t-1]][cur.x[j]]!=INF&&cur.cl+m[cur.x[t-1]][cur.x[j]]<bestl)
            {
                temp=node(cur.cl+m[cur.x[t-1]][cur.x[j]],t+1);
                //如果找到了一个下级节点,那么该节点到现在为止和同级的节点路径相同,除了当前这一级的不同
                for(int k=1; k<=n; ++k)
                    temp.x[k]=cur.x[k];
                swap(temp.x[t],temp.x[j]);
                q.push(temp);
            }
        }
    }
}

3.不同规模案例生成

        顶点个数为n,所以可能有n*n个权重。

        但是可能有一些节点之间无可达边,所以下面还设置了一个参数probability,作为矩阵中不可达边的概率,来设置权重矩阵的“稀疏”程度。

        而且由于INF的存在,所以权重并不能无限制的大。而且我们也没有需求将这类值设的很大(不影响复杂度),不妨限制在0-100之间的整数。

// 设置概率值,矩阵中不可达边的概率 
   		 double probability = 0.1;
		// c数组 不妨设置权重范围为(a,b] 
		int a=0,b=100;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++) {
				 // 生成一个介于0到1之间的随机小数
    			double randomValue = static_cast<double>(std::rand()) / RAND_MAX;
    			 if (randomValue < probability) {
        			// 有probability的概率输出INF
       				 out<< INF <<' ';
   				}
				else {
			        // 有(1 - probability)的概率输出(a,b]间的整数
			        out<< (rand() % (b-a))+ a + 1<<' ';
    			}
			}
			out<<'\n';
		}

六、测试结果

时间复杂性:O(n!)

        分支限界法解决TSP,搜索树的节点数取决于图中城市的数量。在每一层,都需要考虑从当前城市到所有其他未访问的城市的分支,因此搜索树的规模呈指数级增长。

        具体来说,TSP问题的时间复杂度可以表示为 O(n!),其中 n 是城市的数量。这是因为在每个决策点,需要考虑 n 种可能的下一步选择,而搜索树的深度为 n。

        然而,通过一些优化策略,如减枝和启发式方法,来尽量减少搜索树的规模,以提高算法的效率。在实践中,TSP问题的解决往往依赖于问题的具体实例和算法的实现方式。

n从10-500,每次递增10的测试结果:

自定义输入n的数值的测试结果:

七、实验心得

        通过这次实验,我更为掌握了分支限界法求解TSP问题,并且自己生成案例并测试运行时间的过程中,熟悉了随机化算法和运行时间的计算。

​         实验可改进的地方:随机化过程的加入可能使得不同规模的测试数据与理论值有偏差,可以通过每个输入规模多次测试来减小误差。

八、完整代码

#include <iostream>
#include <cstring>
#include <queue>
#include <fstream>
#include <vector>
#include <windows.h>
#include <time.h>
#define INF 1e7
using namespace std;



//排列树的节点定义
struct node
{
    int cl;//当前走过的路径长度
    int id;//处理的第几个城市
    int x[501];//记录当前路径,下标从1开始

    node() {}
    node(int cl_,int id_)
    {
        cl=cl_;
        id=id_;
        memset(x,0,sizeof(x));
    }
};



//用于构建最小堆
struct cl_cmp {
    //当前路径长度短的优先级高
    bool operator()(node n1, node n2)
    {
        return n1.cl > n2.cl;
    }
};

void bfs(int n,int &bestl,vector<int> &bestx,vector<vector<int> > &m)
{
    //选用最小堆
    priority_queue<node,vector<node>,cl_cmp> q;
    //创建一个节点,从该节点开始,因为1是固定位,其实是从1开始探索
    node temp(0,2);
    //初始化解向量
    for(int i=1; i<=n; ++i)
        temp.x[i]=i;
    q.push(temp);
    node cur;//当前节点,也就是活节点
    int t;
    while(!q.empty())
    {
        cur=q.top();
        q.pop();
        t=cur.id;
        if(t==n)
        {
            //满足约束条件,有路径
            //检测图G是否存在一条从顶点x[n-1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边
            if(m[cur.x[t-1]][cur.x[t]]!=INF&&m[cur.x[t]][1]!=INF)
            {
                if(cur.cl+m[cur.x[t-1]][cur.x[t]]+m[cur.x[t]][1]<bestl)
                {
                    //更新最优解和最优路径
                    bestl=cur.cl+m[cur.x[t-1]][cur.x[t]]+m[cur.x[t]][1];
                    for(int i=1; i<=n; ++i)
                        bestx[i]=cur.x[i];
                }
            }continue;
        }
        //大于等于最优路径,没必要继续探索了,从下一个节点开始
        if(cur.cl>=bestl)
            continue;
        //从当前节点开始探索t-1 -> t,t+1,t+2...
        for(int j=t; j<=n; ++j)
        {
            //满足约束条件和限界条件
            if(m[cur.x[t-1]][cur.x[j]]!=INF&&cur.cl+m[cur.x[t-1]][cur.x[j]]<bestl)
            {
                temp=node(cur.cl+m[cur.x[t-1]][cur.x[j]],t+1);
                //如果找到了一个下级节点,那么该节点到现在为止和同级的节点路径相同,除了当前这一级的不同
                for(int k=1; k<=n; ++k)
                    temp.x[k]=cur.x[k];
                swap(temp.x[t],temp.x[j]);
                q.push(temp);
            }
        }
    }
}

int main(){
	ofstream out1("output.txt");
	int n=500;
	while(n-=10){
//		//生成规模为n的随机数 
//		cout<<"请输入顶点个数n:"<<endl; 
//		int n;
//		cin>>n;
		ofstream out("input1.txt");
		out<<n<<'\n';
		srand((unsigned)time(NULL));
		

    	// 设置概率值,矩阵中不可达边的概率 
   		 double probability = 0.1;

		// c数组 不妨设置权重范围为(a,b] 
		int a=0,b=100;
		int i,maxi,mini;
		LARGE_INTEGER nFreq,nBegin,nEnd;
		double time; 
		QueryPerformanceFrequency(&nFreq);	
		QueryPerformanceCounter(&nBegin);
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++) {
				 // 生成一个介于0到1之间的随机小数
    			double randomValue = static_cast<double>(std::rand()) / RAND_MAX;
    			 if (randomValue < probability) {
        			// 有probability的概率输出INF
       				 out<< INF <<' ';
   				}
				else {
			        // 有(1 - probability)的概率输出(a,b]间的整数
			        out<< (rand() % (b-a))+ a + 1<<' ';
    			}
			}
			out<<'\n';
		}
		QueryPerformanceCounter(&nEnd);
		time=(double)(nEnd.QuadPart-nBegin.QuadPart)/(double)nFreq.QuadPart; 
		
		cout<<"生成时间:"<<time<<endl;
		out.close();
	

		
		ifstream in("input1.txt");
		
		in>>n;
//		cout<<"n= "<<n<<endl; 
 
    	
		vector<vector<int> > m(n+1, vector<int>(n+1));
		vector<int> bestx(n+1);
		int bestl=INF;//最优解长度
		for(i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				in>>m[i][j];
//			cout<<m[i][j]<<" ";
			}
//			cout<<endl;
		}

		QueryPerformanceFrequency(&nFreq);	
		QueryPerformanceCounter(&nBegin);
		bfs(n, bestl, bestx, m);    	
		QueryPerformanceCounter(&nEnd);
		time=(double)(nEnd.QuadPart-nBegin.QuadPart)/(double)nFreq.QuadPart; 
		cout<<"最优值为:"<<bestl<<endl;
    	cout<<"最优解为:";
    	for(int i=1; i<=n; ++i)
        	cout<<bestx[i]<<" ";
   		cout<<bestx[1]<<endl;		
		cout<<"查询时间:"<<time<<endl<<endl;
		out1<<n<<' '<<time<<endl;
		in.close();		
	}
	out1.close();
	return 0;
}

九、绘图代码

import matplotlib.pyplot as plt

def read_file(filename):
    data = []
    with open(filename, 'r') as file:
        for line in file:
            x, y = map(float, line.split())
            data.append((x, y))
    return data

# 读取文件的数据
file3_data = read_file('F:\\3-CourseMaterials\\3-1\\3-算法设计与分析\实验\lab4\\1-code\\2-分支限界法求解TSP问题\\output.txt')

# 分别提取 x 和 y 的值
file3_x, file3_y = zip(*file3_data)
print(file3_x)
print(file3_y)

# 绘制图形,指定不同颜色
# plt.plot(file1_x, file1_y, label='Solution 1', color='red')
# plt.plot(file2_x, file2_y, label='Solution 2', color='green')
plt.plot(file3_x, file3_y, label='Solution 1', color='red')

# 添加图例、标签等
plt.legend()
plt.title('Data from Three Files')
plt.xlabel('X-axis')
plt.ylabel('Y-axis')

# 显示图形
plt.show()


                      

  • 26
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值