旅行商问题:分支界限(优先队列)

什么是旅行商问题

旅行商问题(TravelingSalesmanProblem,TSP)是一个经典的组合优化问题。经典的TSP可以描述为:一个商品推销员要去若干个城市推销商品,该推销员从一个城市出发,需要经过所有城市后,回到出发地。应如何选择行进路线,以使总的行程最短。
从图论的角度来看,该问题实质是在一个带权完全无向图中,找一个权值最小的Hamilton回路。由于该问题的可行解是所有顶点的全排列,随着顶点数的增加,会产生组合爆炸,它是一个NP完全问题。由于其在交通运输、电路板线路设计以及物流配送等领域内有着广泛的应用,国内外学者对其进行了大量的研究。早期的研究者使用精确算法求解该问题,常用的方法包括:分枝定界法、线性规划法、动态规划法等。但是,随着问题规模的增大,精确算法将变得无能为力,因此,在后来的研究中,国内外学者重点使用近似算法或启发式算法,主要有遗传算法、模拟退火法、蚁群算法、禁忌搜索算法、贪婪算法和神经网络等。

为什么用分支界限法?

为什么要用分支界限法(优先队列)来解决这个问题呢?因为分支界限(优先队列)本质上是一种暴力+技巧的一种算法,如果技巧十分的巧妙,那么求解的问题往往效果非常的好,另外和回溯法和暴力法不同的时候,在你优先队列的优先级确定的十分巧妙的时候,能够保证第一个出队列的叶节点它就是最优解(有解的情况下),这样能够大大减少计算的时间。

优先级的定义

这个优先级的定义稍微有点儿抽象,但你可以这样想,首先定义一个与顶点数量同样大的数组MinOut,它用来存放从该节点到达其他有路径结点的最小费用。再用MinSum计算这个最小费用数组的费用总和,可以确定这个总和一定是最小最小的费用,但是这个费用对应的路径不一定能够走得通,具体代码如下:

	Type* MinOut = new Type[n + 1];
	Type MinSum = 0;			//最小出边费用和
	for (int i = 1; i <= n; i++) {
		Type Min = NoEdge;
		for (int j = 1; j <= n; j++) {
			if (a[i][j] != NoEdge && (a[i][j] < Min || Min == NoEdge))		//找出那个最小出边
				Min = a[i][j];
		}
		if (Min == NoEdge)		//不存在回路
			return NoEdge;
		MinOut[i] = Min;		//MinOut是用来记录最小出边的
		MinSum += Min;			//最小出边费用和
	}

知道MinSum的意义之后,接着这样想,假设s是我们正在或者刚到达的城市,假设从第s到第n个城市的最小费用用rcost定义,它的初始值为MinSum。每次到达一个城市,MinSum就减去s城市对应的最小费用即减去MinOut[s],并且这个计算是从第一个城市开始的,于是有:

rcost = E.rcost - MinOut[E.x[E.s]];

同时定义优先级为lcost,它指的是从第一个城市到第s个城市的费用(这是一个确定的值),它加上rcost这时候不就是走完回路的最小费用,而它就是这条路径的下界,也就是我们优先级的定义:

lcost = cc + rcost;//cc指的是到达s城市之前的费用

理解完以上的关键部分,有以下的代码:

#include <iostream>
#include <queue>
using namespace std;


template <class Type>
class Traveling {
	//friend void main(void);
public:
	Type BBTSP(int v[]);
	int n;			//图G的顶点数
	Type** a,		//图G的零阶矩阵
		NoEdge;		//图G的无边标志
		//cc,			//当前费用
		//bestc;		//当前最小费用
};


//最小堆表示活结点优先队列,最小堆的元素类型是MinHeapNode
template <class Type>
class MinHeapNode {
	friend Traveling<Type>;
public:
	Type lcost,		//子树费用的下界
		cc,			//当前费用	
		rcost;		//x[s:n-1]中顶点最小出边费用和
	int s,			//根节点到当前结点的路径为x[0:s]
		* x;		//需要进一步搜索的顶点是x[s+1:n-1]

	bool operator <(const MinHeapNode& p)const
	{
		return p.lcost < lcost;//目标函数值小的先出队列
	}
};

/*
	堆中的每个结点lcost值是优先队列的优先级
	每个顶点的最小费用出边用Minout记录
	如果顶点没有出边,则不存在回路
*/

template <class Type>
Type Traveling<Type>::BBTSP(int v[]) {
	priority_queue<MinHeapNode<Type>>H;
	Type* MinOut = new Type[n + 1];
	Type MinSum = 0;			//最小出边费用和
	for (int i = 1; i <= n; i++) {
		Type Min = NoEdge;
		for (int j = 1; j <= n; j++) {
			if (a[i][j] != NoEdge && (a[i][j] < Min || Min == NoEdge))		//找出那个最小出边
				Min = a[i][j];
		}
		if (Min == NoEdge)		//不存在回路
			return NoEdge;
		MinOut[i] = Min;		//MinOut是用来记录最小出边的
		MinSum += Min;			//最小出边费用和
	}

	//初始化
	MinHeapNode <Type> E;		//创建一个新节点
	E.x = new int[n];
	for (int i = 0; i < n; i++)
		E.x[i] = i + 1;
	E.s = 0;					//走过的结点的数量
	E.cc = 0;					//当前费用初始化为0
	E.rcost = MinSum;			
	Type bestc = NoEdge;
	
	//搜索排列空间树
	while (E.s < n - 1) {		//非叶节点
		if (E.s == n - 2) {		//叶节点的父节点(排列问题嘛,还剩一个结点没排列的时候其实已经是固定的了)
			if (a[E.x[n - 2]][E.x[n - 1]] != NoEdge && a[E.x[n - 1]][1] != NoEdge &&//更新的条件是,倒数第二个结点到最后一个结点有边,最后一个结点到出发点有边
				(E.cc + a[E.x[n - 2]][E.x[n - 1]] + a[E.x[n - 1]][1] < bestc || bestc == NoEdge)) {//同时这个费用小于当前费用,或者bestc一直为无穷大
				bestc = E.cc + a[E.x[n - 2]][E.x[n - 1]] + a[E.x[n - 1]][1];
				E.cc = bestc;
				E.lcost = bestc;
				E.s++;
				H.push(E);
			}
			else
				delete[] E.x;	//舍弃扩展结点
		}
		else {
			for (int i = E.s + 1; i < n; i++) {//还剩(s,n)没有去过
				if (a[E.x[E.s]][E.x[i]] != NoEdge) {//如果存在边,就成为可能的可行结点
					Type cc = E.cc + a[E.x[E.s]][E.x[i]];
					Type rcost = E.rcost - MinOut[E.x[E.s]];//每次走过一个城市,就减去对应的MinOut(这个城市到其他城市的最小费用),这里需要想想
					Type b = cc + rcost;			//得到下界
					if (b < bestc || bestc == NoEdge) {//剪枝策略
						MinHeapNode <Type> N;
						N.x = new int[n];
						for (int j = 0; j < n; j++)
							N.x[j] = E.x[j];
						//交换一下位置,因为这个位于i位置上的城市作为下一个去的城市,它可能产生最优解
						N.x[E.s + 1] = E.x[i];
						N.x[i] = E.x[E.s + 1];
						N.cc = cc;
						N.s = E.s + 1;
						N.lcost = b;
						N.rcost = rcost;
						H.push(N);
					}
				}
			}
			delete[]E.x;//完成结点扩展
		}
		if (H.empty())
			break;
		E = H.top();
		H.pop();
	}
	if (bestc == NoEdge)
		return NoEdge;
	for (int i = 0; i < n; i++) {//最优解复制到v
		v[i + 1] = E.x[i];
	}
	while (true) {
		delete[]E.x;
		if (H.empty())
			break;
		E = H.top();
		H.pop();
	}
	return bestc;
}

int main() {
	Traveling <int> T;
	cin >> T.n;
	T.NoEdge = INT_MAX;
	int* v = new int[T.n + 1];
	T.a = (int**) new int* [T.n + 1];
	for (int i = 0; i <= T.n; i++) {
		T.a[i] = new int[T.n + 1];
	}
	for (int i = 1; i <= T.n; i++)
	{
		for (int j = 1; j <= T.n; j++)
		{
			cin >> T.a[i][j];
			if (i == j)
			{
				T.a[i][j] = INT_MAX;
			}
		}
	}
	cout << T.BBTSP(v) << endl;
	return 0;
}

/*测试
5
100000 5 61 34 12
57 100000 43 20 7
39 42 100000 8 21
6 50 42 100000 8
41 26 10 35 100000
36
请按任意键继续. . .
*/
  • 3
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
行商问题是一个经典的组合优化问题,它的目标是在访问所有城市并返回起点的情况下,寻找最短的行路线。分支界限法是一种广泛应用于求解组合优化问题的方法,下面是使用C++实现行商问题分支界限算法: ```c++ #include <iostream> #include <queue> #include <vector> using namespace std; // 城市数量 const int MAXN = 10; // 行商问题类 class TSP { public: TSP() {} ~TSP() {} void init(); // 初始化城市距离矩阵 void solve(); // 求解行商问题 private: int n; // 城市数量 int d[MAXN][MAXN]; // 城市距离矩阵 int best_cost; // 最优解的花费 vector<int> best_path; // 最优解的路径 // 状态类 class State { public: State(int _cost, vector<int> _path) { cost = _cost; path = _path; } ~State() {} bool operator<(const State& s) const { return cost > s.cost; } int cost; // 花费 vector<int> path; // 路径 }; // 分支界限法求解 void branch_and_bound(); }; // 初始化城市距离矩阵 void TSP::init() { n = 5; int dis[MAXN][MAXN] = { { 0, 2, 3, 4, 5 }, { 2, 0, 3, 4, 5 }, { 3, 3, 0, 4, 5 }, { 4, 4, 4, 0, 5 }, { 5, 5, 5, 5, 0 } }; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { d[i][j] = dis[i][j]; } } } // 求解行商问题 void TSP::solve() { best_cost = 1e9; best_path.clear(); branch_and_bound(); cout << "最优解的花费为:" << best_cost << endl; cout << "最优解的路径为:"; for (int i = 0; i < n; i++) { cout << best_path[i] << " "; } cout << endl; } // 分支界限法求解 void TSP::branch_and_bound() { priority_queue<State> q; // 优先队列 vector<int> init_path; init_path.push_back(0); State init_state(0, init_path); q.push(init_state); while (!q.empty()) { State s = q.top(); q.pop(); if (s.cost >= best_cost) continue; // 剪枝 if (s.path.size() == n) { // 更新最优解 s.cost += d[s.path[n - 1]][0]; if (s.cost < best_cost) { best_cost = s.cost; best_path = s.path; } continue; } for (int i = 0; i < n; i++) { bool flag = true; for (int j = 0; j < s.path.size(); j++) { if (i == s.path[j]) { flag = false; break; } } if (flag) { vector<int> new_path = s.path; new_path.push_back(i); State new_state = State(s.cost + d[s.path.back()][i], new_path); q.push(new_state); } } } } int main() { TSP tsp; tsp.init(); tsp.solve(); return 0; } ``` 上述代码中,`TSP` 类封装了行商问题的求解过程,其中 `init()` 函数用于初始化城市距离矩阵,`solve()` 函数用于求解行商问题,`branch_and_bound()` 函数实现了分支界限法求解过程。 在 `branch_and_bound()` 函数中,使用了优先队列来维护候选状态,每次取出花费最小的状态进行扩展。在扩展状态时,如果当前状态的花费已经超过了当前最优解的花费,则进行剪枝,不再继续扩展。如果当前状态已经访问了所有城市,则将当前状态作为最优解进行更新。否则,将当前状态的每个未访问城市作为下一步的选择,生成新的状态,并加入优先队列中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

call me Patrick

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

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

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

打赏作者

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

抵扣说明:

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

余额充值