7-2 旅行售货员 (10 分)(TSP问题思路加详解)

一题目

某售货员要到若干城市去推销商品,已知各城市之间的路程(或旅费)。他要选定一条从驻地出发,经过每个城市一遍,最后回到驻地的路线,使总的路程(或总旅费)最小。

输入格式:
第一行为城市数n

下面n行n列给出一个完全有向图,如 i 行 j 列表示第 i 个城市到第 j 个城市的距离。

输出格式:
一个数字,表示最短路程长度。

输入样例:

3
0 2 1
1 0 2
2 1 0

输出样例:

3

二:区别回溯法和分支限界法

分支限界法类似于回溯法,也是一种在问题的解空间树T上搜索问题解的算法。但在一般情况下,分支限界法与回溯法的求解目标不同。回溯法的求解目标是找出T中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。
即回溯法是想求出所有的可行解,对解的空间树搜索方式为 DFS
而分支限界想求出的解只有一个最优解,对解的空间树的搜索方式为 BFS
在这里插入图片描述

三:思路

思路:
1.注意题目给出的完全有向图:
有向完全图是指概述图中各边都有方向,且每两个顶点之间都有两条方向相反的边连接的图。
2.通过分析据我们不难发现,每两个顶点间的距离是不一样的
3.写码思路:
<1>:本题解空间是排列树
<2>:我们在遍历树的时候我们采用的BFS,采用的数据结构是优先队列。
<3>:每次在选择扩展结点的时候,我们自定义的排序规则是将已有路径最小的先选择
<4>:选完扩展结点的时候,我们就要处理该扩展结点的邻接点,到达邻接点的距离小于bestw,我们就将该邻接点
统计到队列中
同时更新到达该结点路径程度,记录已经选择的路径顺序(x[i]),因为我们每次选择
的结点不同,所以我们记录的路径顺序也会有变化,那么就需要swap(x[t],x[j]),
这时候,我们就能得到正确的路径顺序。
<5>:当我们要遍历到叶节点的时候,这时候就要更新我们的bestw,将到达最后一个结点的距离加上
最后一个结点到源结点的距离 和 bestw进行比较更新,比其小就更新bestw,
否则,就跳过本轮循环,因为我们我们已经知道了其再往下走,也是不可能比bestw更小了
在这里插入图片描述
在这里插入图片描述

四:上码

/**
	思路:
	  1.注意题目给出的完全有向图: 
 	     有向完全图是指概述图中各边都有方向,且每两个顶点之间都有两条方向相反的边连接的图。
	  2.通过分析据我们不难发现,每两个顶点间的距离是不一样的 
	  3.写码思路:
	  	 <1>:本题解空间是排列树
		 <2>:我们在遍历树的时候我们采用的BFS,采用的数据结构是优先队列。
		 <3>:每次在选择扩展结点的时候,我们自定义的排序规则是将已有路径最小的先选择
		 <4>:选完扩展结点的时候,我们就要处理该扩展结点的邻接点,到达邻接点的距离小于bestw,我们就将该邻接点
		 	统计到队列中 
		 	 同时更新到达该结点路径程度,记录已经选择的路径顺序(x[i]),因为我们每次选择
			  的结点不同,所以我们记录的路径顺序也会有变化,那么就需要swap(x[t],x[j]),
			  这时候,我们就能得到正确的路径顺序。
		 <5>:当我们要遍历到叶节点的时候,这时候就要更新我们的bestw,将到达最后一个结点的距离加上
		 	  最后一个结点到源结点的距离 和 bestw进行比较更新,比其小就更新bestw,
		   	 否则,就跳过本轮循环,因为我们我们已经知道了其再往下走,也是不可能比bestw更小了
		 			   

*/ 
#include<bits/stdc++.h> 
using namespace std;

int INF=999999999;
const int N=100;  //const  初始化 
int maps[N][N];   //存储图
int n,m;         //n表示顶点数,m表示边数
int bestw;

struct Node
{
    int x[N];    //解向量,方便从1开始,记录路径
    int cl;      //表示当前已走过的路径长度
    int id;     //表示层数
};

//重写 优先队列当中排序方法  按每次路径最短的升序处理 
bool operator<(const Node &a,const Node &b){
    return a.cl>b.cl;
}

void bfs()
{
    priority_queue<Node>q;   
	 Node node;
	 node.cl = 0;   // 0 表示当前已经的走的路径长度,2表示层数
	 node.id = 2;
	
	//表示的是每个结点的序号,用于记录树当中路径 
    for(int i=1;i<=n;i++){
        node.x[i]=i;
    }
    
    q.push(node);
    
    while(!q.empty()){
    	
    	//这个新结点的信息就是上方的node,其解向量为 1,2,3,4...而c = 0 id = 2;  
        Node newnode=q.top();
        q.pop();
        
        int t;
        t = newnode.id;//当前层数
        
        if(t==n){//当n==4的时候,其实是走了3个结点两个距离,所以还需要判断与最后一个结点
				//的距离,以及最后一个结点和首节点的距离。
				
			//如果两个结点中有一个的距离是无穷的那么,它的路径长度肯定不满足要求	 
            if(maps[newnode.x[t-1]][newnode.x[n]] != INF 
			&& maps[newnode.x[n]][newnode.x[1]] != INF ){
			
			//如果满足要求的话,那么就要判断已有的路径长度+到最后一个结点长度
			//+最后一个结点到源结点的距离。	
             if(newnode.cl + maps[newnode.x[t-1]][newnode.x[n]]
                   + maps[newnode.x[n]][newnode.x[1]] < bestw){
                   	
                   bestw = newnode.cl+maps[newnode.x[t-1]][newnode.x[n]]
                   + maps[newnode.x[n]][newnode.x[1]];//更新bestw;
                } else{//如果我们已经知道其最后的总路程是大于bestw的话,那就没有必要再统计其邻接点了 
                	continue;
				}
            }else{//这里也是,如果我们已经知道其到达最后一个结点,或是最后一个结点到达首结点
				  //的距离是无穷的,那么我们就没必要再往下统计了 
            	continue;
			} 
        } 
        
        if(newnode.cl >= bestw) continue;//限界条件
        
        //拿出队列当中的头节点,扩展其所有的分支 
        for(int j = t; j <= n; j++){//扩展所有分支
        
        	//这里是将当前的结点的临界点遍历,但是需要比较已有的路径
			//长度跟当前的最优值,比其小才遍历,否则的话,再往下进行肯定是比最优值大的 
           //因为结点层数是比矩阵当中记录的行数多1,所以要减去。 
		    if(newnode.cl + maps[newnode.x[t-1]][newnode.x[j]] < bestw){
            	
                int c = newnode.cl + maps[newnode.x[t-1]][newnode.x[j]];
                
                //生成一个新的结点 且更新现在的路径长度和遍历的树的层数 
                Node node;
                node.cl = c;
                node.id = t+1;
				//复制父节点的解向量 
                for(int i = 1; i <= n; i++)
                    node.x[i] = newnode.x[i];
                 
				//这个交换是为了  扩展结点可以在下一次的加入邻接点(活结点)的时候 
				//可以正确统计该活结点在矩阵当中的正确行数   
                swap(node.x[t],node.x[j]);
                
                q.push(node);
            }
        }
    }

}
int main(){
	
    cin>>n;
    for(int i=1;i<=n;i++){
    	for(int j = 1; j <= n; j++)   {
    		int w;
		    cin >> w;
		    if(w == 0)
		    	maps[i][j] = INF;
		    else
		    	maps[i][j] = w;	
		}
    	
    }
    bestw=INF;
    bfs();   
    cout << bestw;
}


//3
//0 2 1
//1 0 2
//2 1 0


//3 6
//1 2 2
//1 3 1
//2 1 1
//2 3 2
//3 1 2
//3 2 1


//4 6
//1 2 15
//1 3 30
//1 4 5
//2 3 6
//2 4 12
//3 4 3


//0 15 30 5
//0 0 6 12
//0 0 0 3
//0 0 0 0

在这里插入图片描述

五:总结

下方的博客在处理剪枝的操作并不完美,也就是处理到 最后一个结点和最后一个结点到到首结点的操作有漏洞,我进行了修改,其他地方都值得学习!!
学习自本篇博客

  • 15
    点赞
  • 72
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
旅行售货员问题,又称旅行推销员问题(Traveling Salesman Problem, TSP),是指一个售货员要去若干城市推销商品,他必须从一个城市出发,经过所有城市后再回到出发城市,而且每个城市只能去一次,且最终完成任务的总距离要最短。 旅行售货员问题是一个经典的组合优化问题,它的解空间非常庞大,因此解决方法多样,其中一种常用的方法是支限定法。 支限定法是一种穷举法,通过不断割问题的解空间,排除不可能的解,最终得到问题的最优解。在旅行售货员问题中,支限定法的思路如下: 1. 首先,根据问题的情况确定问题规模,并初始化某些变量,如最短路径长度、当前路径长度等。 2. 然后,选择一个起始城市,并将其标记为已访问。 3. 对于每个未访问的城市,按照某种规则(如距离最近)选择一个城市,并将其标记为已访问。 4. 计算当前路径长度,如果当前路径长度已经大于已知的最短路径长度,则剪枝,回溯到上一步。 5. 如果所有城市都已经访问完毕,并且当前路径长度小于最短路径长度,则更新最短路径长度,保存当前路径。 6. 回溯到上一步,并继续选择下一个未访问的城市。 7. 重复步骤3-6,直到找到所有可能的路径。 8. 最后,从保存的路径中选出最短路径,即为问题的最优解。 支限定法的时间复杂度为O(n!),其中n为城市的数量。由于旅行售货员问题是一个NP困难问题,没有多项式时间的解决方法。因此,在实际应用中,往往需要使用一些启发式算法或近似算法来求解。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天向上的菜鸡杰!!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值