【旅行商售货员问题】“分支限界法”——《算法设计与分析(第五版)》


一、算法要求

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

1. 思路

现在我们从景点A出发,要去B、C、D、E共4个景点,按上面顺序给景点编号1~5,每个景点用一个结点表示,可以直接到达的景点有连线,连线上的数字代表两个景点之间的路程(时间)。那么要去的景点地图就转化成了一个无向带权图。
在无向带权图G=(V,E)中,结点代表景点,连线上的数字代表景点之间的路径长度。我们从1号结点出发,先走哪些景点,后走哪些景点呢?只要是可以直接到达的,即有边相连的,都是可以走的。问题就是要找出从出发地开始的一个景点排列,按照这个顺序旅行,不重复地走遍所有景点回到出发地,所经过的路径长度是最短的。
因此,问题的解空间是一棵排列树。显然,对于任意给定的一个无向带权图,存在某两个景点之间没有直接路径的情况。也就是说,并不是任何一个景点排列都是一条可行路径(问题的可行解),因此需要设置约束条件,判断排列中相邻的两个景点之间是否有边相连,有边的则可以走通;反之,不是可行路径。另外,在所有的可行路径中,要求找出一条最短路径,因此需要设置限界条件。
在这里插入图片描述


二、完整代码

1. 主文件

main.cpp:

// Project4: 着色问题

#include"Improve1.h"
#include <iostream>
#include <iomanip>
using namespace std;

const int N = 4;//图的顶点数  

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

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


//优先队列式分支限界法
template<class Type>
Type Traveling<Type>::BBTSP(int v[]){
	MinHeap<MinHeapNode<Type>> chessBoard(1000);		//定义最小堆的容量为1000
	Type* MinOut = new Type[n + 1];
	//计算MinOut[i] = 顶点i的最小出边费用
	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;
		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;			//根节点到当前节点路径为x[0:s]
	E.cc = 0;			//当前费用
	E.rcost = MinSum;	//最小出边费用和
	Type bestc = NoEdge;

	//搜索排列空间树
	while (E.s < n - 1) {			//非叶结点
		if (E.s == n - 2){			//当前扩展节点是叶节点的父节点
			//再加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 = 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++;
				chessBoard.Insert(E);
			}
			else{
				delete[] E.x;		//舍弃扩展节点
			}
		}

		else{						//产生当前扩展节点的儿子节点
			for (int i = E.s + 1; i < n; i++) {
				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]];
					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];
						}
						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;
						chessBoard.Insert(N);
					}
				}
			}
			delete[]E.x;			//完成节点扩展
		}
		if (chessBoard.Size() == 0) {
			break;					//堆空
		}
		chessBoard.DeleteMin(E);				//取下一扩展节点
	}

	if (bestc == NoEdge) {
		return NoEdge;//无回路
	}
	//将最优解复制到v[1:n]
	for (int i = 0; i < n; i++) {
		v[i + 1] = E.x[i];
	}

	while (true) {//释放最小堆中所有节点
		delete[]E.x;
		if (chessBoard.Size() == 0) {
			break;					//堆空
		}
		chessBoard.DeleteMin(E);				//取下一扩展节点
	}
	return bestc;
}


int main()
{
	int bestx[N + 1],
		** a = new int* [N + 1],		//用a[][]存储邻接矩阵
		simpleArray[4][4] = { {0,30,6,4},
							{30,0,5,10} ,
							{6,5,0,20} ,
							{4,10,20,0} };

	cout << "\nThe number of vertices in the graph: n = " << N << endl;

	//初始化对应位置的邻接矩阵
	for (int i = 0; i <= N; i++) {
		a[i] = new int[N + 1];
	}
	//输出数据输入情况
	cout << "\nThe adjacency matrix of the graph is as follows: " << endl;
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < N; j++) {
			a[i + 1][j + 1] = simpleArray[i][j];
			cout << "|" << setw(4) << a[i + 1][j + 1];
		}
		cout << "|" << endl;
	}

	Traveling<int> example;
	example.a = a;
	example.n = N;

	cout << "\n#The length of the shortest loop is:" << example.BBTSP(bestx) << endl;
	cout << "\n#The path of the shortest loop is:";
	for (int i = 1; i <= N; i++) {
		cout << setw(3) << bestx[i] << "  ==>";
	}
	cout << setw(3) << bestx[1] << endl;

	//析构,**a删除申请的空间
	for (int i = 0; i < N + 1; i++) {
		delete[]a[i];
	}
	delete[]a;

	return 0;
}

2. 头文件

Improve1.h:

#pragma once
#ifndef __IMPROVE1__
#define __IMPROVE1__
#include <iostream>
using namespace std;

//============================最小堆============================//
template<class T>
class MinHeap{	//最小堆
public:
	MinHeap(int maxheapsize = 10) {
		maxsize = maxheapsize;
		heap = new T[maxsize + 1];
		currentsize = 0;
	}

	~MinHeap() { 
		delete[]heap; 
	}

	int Size() const { 
		return currentsize; 
	}

	T Max() { 
		if (currentsize)
			return heap[1]; 
}

	MinHeap<T>& Insert(const T& x);
	MinHeap<T>& DeleteMin(T& x);

	void Initialize(T x[], int size, int ArraySize);
	void Deactivate();
	void Output(T a[], int n);

private:
	int currentsize, maxsize;
	T* heap;
};

template <class T>
void MinHeap<T>::Output(T a[], int n) {
	for (int i = 1; i <= n; i++)
		cout << a[i] << " ";
	cout << endl;
}



template<class T>
MinHeap<T>& MinHeap<T>::Insert(const T& x) {//插入
	if (currentsize == maxsize) {
		return *this;
	}
	int i = ++currentsize;

	while (i != 1 && x < heap[i / 2]) {
		heap[i] = heap[i / 2];
		i /= 2;
	}

	heap[i] = x;
	return *this;
}

template<class T>
MinHeap<T>& MinHeap<T>::DeleteMin(T& x)
{
	if (currentsize == 0)
	{
		cout << "Empty heap!" << endl;
		return *this;
	}

	x = heap[1];

	T y = heap[currentsize--];
	int i = 1, ci = 2;
	while (ci <= currentsize)
	{
		if (ci < currentsize && heap[ci] > heap[ci + 1])
		{
			ci++;
		}

		if (y <= heap[ci])
		{
			break;
		}
		heap[i] = heap[ci];
		i = ci;
		ci *= 2;
	}

	heap[i] = y;
	return *this;
}

template<class T>
void MinHeap<T>::Initialize(T x[], int size, int ArraySize) {//初始化
	delete[]heap;
	heap = x;
	currentsize = size;
	maxsize = ArraySize;

	for (int i = currentsize / 2; i >= 1; i--) {
		T y = heap[i];
		int c = 2 * i;
		while (c <= currentsize) {
			if (c < currentsize && heap[c] > heap[c + 1])
				c++;
			if (y <= heap[c])
				break;
			heap[c / 2] = heap[c];
			c *= 2;
		}
		heap[c / 2] = y;
	}
}

template<class T>
void MinHeap<T>::Deactivate() {
	heap = 0;
}

#endif



3. 效果展示

在这里插入图片描述


三、补充

(1)时间复杂度
最坏情况下,如图6-46所示。除了最后一层外,有1+n+n(n-1)+…+(n-1)(n-2)…2≤n(n-1)!个结点需要判断约束函数和限界函数,判断两个函数需要O(1)的时间,因此耗时O(n!),时间复杂度为O(n!)。
(2)空间复杂度
程序中我们设置了每个结点都要记录当前的解向量x[]数组,占用空间为O(n),结点的个数最坏为O(n!),所以该算法的空间复杂度为O(n*n!)。

文档供本人学习笔记使用,仅供参考。

  • 4
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
旅行售货员问题是NP完全问题中的一个经典问题,其目的是在给定一组城市和它们之间的距离矩阵的情况下,求出一条经过每个城市恰好一次的最短路径。而分支限界法是一种搜索算法,它可以通过剪枝来减少搜索空间,从而提高搜索效率。下面是使用C++实现旅行售货员问题的分支限界算法的代码: ```c++ #include <iostream> #include <vector> #include <queue> #include <algorithm> #include <limits> using namespace std; // 旅行售货员问题结构体 struct TSP { vector<vector<int>> dist; // 城市之间的距离矩阵 int n; // 城市数量 int min_cost; // 最小花费 vector<int> path; // 最小花费下的路径 }; // 结点结构体 struct Node { int level; // 结点所在层数(当前访问的城市编号) int cost; // 到达当前城市的花费 vector<int> path; // 到达当前城市的路径 bool visited[20]; // 标记已经访问过的城市 double bound; // 当前结点的花费下界 bool operator<(const Node& other) const { // 重载小于号,用于STL最小堆排序 return bound < other.bound; } }; // 计算结点的花费下界 double calc_bound(const TSP& tsp, Node& node) { double bound = node.cost; int level = node.level; // 计算已经访问过的城市到未访问过的城市的最小距离和 for (int i = 0; i < tsp.n; i++) { if (!node.visited[i]) { int min_dist = numeric_limits<int>::max(); for (int j = 0; j < tsp.n; j++) { if (i != j && node.visited[j]) { min_dist = min(min_dist, tsp.dist[j][i]); } } bound += min_dist; } } return bound; } // 分支限界法求解旅行售货员问题 void tsp(TSP& tsp) { // 初始化根结点 Node root = {0, 0, vector<int>(1, 0), {true}, 0}; root.bound = calc_bound(tsp, root); // 初始化最小堆 priority_queue<Node> Q; Q.push(root); // 开始搜索 while (!Q.empty()) { Node cur = Q.top(); Q.pop(); if (cur.bound >= tsp.min_cost) { // 当前结点的花费下界大于等于已经找到的最小花费,剪枝 continue; } if (cur.level == tsp.n - 1) { // 已经访问了所有城市 cur.cost += tsp.dist[cur.path.back()][0]; if (cur.cost < tsp.min_cost) { // 更新最小花费 tsp.min_cost = cur.cost; tsp.path = cur.path; } continue; } // 分别考虑从当前城市出发访问所有未访问过的城市的情况 for (int i = 1; i < tsp.n; i++) { if (!cur.visited[i]) { Node child = cur; child.level++; child.cost += tsp.dist[child.path.back()][i]; child.path.push_back(i); child.visited[i] = true; child.bound = calc_bound(tsp, child); if (child.bound < tsp.min_cost) { // 只将花费下界小于最小花费的子结点加入最小堆中 Q.push(child); } } } } } int main() { TSP tsp = {{ {0, 10, 15, 20}, {10, 0, 35, 25}, {15, 35, 0, 30}, {20, 25, 30, 0} }, 4, numeric_limits<int>::max(), {0}}; tsp(tsp); cout << "Min Cost: " << tsp.min_cost << endl; cout << "Path: "; for (int i : tsp.path) { cout << i << "->"; } cout << "0" << endl; return 0; } ``` 在这个代码中,我们定义了一个`TSP`结构体来存储旅行售货员问题的信息,包括城市之间的距离矩阵、城市数量、最小花费和最小花费下的路径。在`Node`结构体中,我们使用一个布尔数组来标记已经访问过的城市,还重载了小于号运算符,这是为了让我们可以使用STL的最小堆来维护搜索结点的优先级。 在`calc_bound`函数中,我们计算了当前结点的花费下界,这是通过贪心的思路来计算的。具体来说,我们首先计算已经访问过的城市到未访问过的城市的最小距离和,然后将当前花费加上这个最小距离和,从而得到当前结点的花费下界。 在`tsp`函数中,我们使用了一个最小堆来维护搜索结点的优先级。在每一次循环中,我们取出最小堆中的顶部结点,然后根据当前结点的状态进行分支限界搜索。具体来说,我们分别考虑从当前城市出发访问所有未访问过的城市的情况,然后计算子结点的花费下界,并将符合条件的子结点压入最小堆中。如果当前结点的花费下界大于等于已经找到的最小花费,则可以剪枝,继续搜索下一个结点。如果已经访问了所有城市,则更新最小花费和最小花费下的路径。 最后,在`main`函数中,我们定义了一个简单的旅行售货员问题实例,然后调用`tsp`函数求解,最终输出结果。 希望这个解答能够帮助到您!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

NI'CE'XIAN

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

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

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

打赏作者

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

抵扣说明:

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

余额充值