动态规划法求解TSP问题 C++

此文章借鉴于博文https://blog.youkuaiyun.com/shujian_tianya/article/details/80873892,在此基础上重新进行了分析总结。

一、问题

在这里插入图片描述

二、想法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三、讲解

1、怎么求顶点子集,即这些怎么记录?在这里插入图片描述
答:例如4个顶点{0,1,2,3},{1,2,3}依次为{},{1},{2},{1,2},{3},{1,3},{2,3},{1,2,3}。十进制数0、1、2、3、4、5、6、7的二进制分别为000、001、010、011、100、101、110、111。上述集合中的元素即为二进制中的位数,例如集合{2,3},可用二进制110(十进制6)代替,因为二进制110的第一位是0,第二位第三位是1。
整理一下思路——十进制数的二进制表示中,哪位为1则集合中就有哪个数。十进制6的二进制110中,第二三位是1,则6代表集合{2,3}
没有为什么,这是我们定下的一条规则,方便我们解题。
如此,过程矩阵d[i][j]的纵坐标j就多了一个含义。j=0,代表集合{ };j=1,代表集合{1}……j=5,代表集合{1,3}……以此类推。
大家可能发现一个问题,在4个顶点{0,1,2,3}中,十进制3代表{1,2},十进制4代表{3},而我们一般写的集合顺序是{},{1},{2},{3},{1,2},{1,3},{2,3},{1,2,3}。集合{3}是第3个,{1,2}是第4个。会发现顺序乱了,这个要说一下,集合之间的排序不影响此题的解答,读者朋友可以将集合以任意顺序写,会发现不影响最终结果的出现。

2、判断一个顶点是否位于子集中
举例解答,如要判断集合j={1,3,5,6,7}是否有顶点3。
集合j={1,3,5,6,7}表示成二进制串为1110101,其中集合里面有的数对应的位数写成1,没有的写成0。要在集合中找顶点3,就是要判断二进制串第3位是不是1,就把1110101右移(3-1)位,得到11101,然后结果和00001进行&运算,如果结果是1说明第3位是1,则说明顶点在子集中。
故判断公式为

(j>>(i-1))&1==1

3、填写过程矩阵过程
arc[i][j]为图的代价矩阵。
d[i][j]为过程矩阵,存放迭代结果。
以d[2][5]为例,d[2][5]=d(2,{1,3})。d[2][5] 表示从2出发,通过{1,3},最后回到起点。那么d[2][5] = min{arc[2][1] + d(1,{3}),arc[2][3] + d(3,{1})} = min{arc[2][1] + d[1][4],arc[2][3] + d[3][1]}。从2出发,要去{1,3},先考虑去1的路。去了1后集合{1,3}中只剩下{3} ,{3}对应二进制100,十进制4,所以要求的d表就是d[1][4],这个4可以通过(101)^(1)得到,而(1) = 1<<(1-1).其中,二进制101(十进制5)代表集合{1,3};再看去3的路,去了3后集合{1,3}中只剩下{1},{1}对应二进制001,十进制1,所以要求的d表就是d[3][1],1通过(101) ^ (100)得到,而(100) = 1<<(3-1)。故此处又总结出一个公式

d[i][j] = min{arc[i][k] + d[k][j ^ (1 << (k - 1))]}

四、代码

#include<iostream>
#include<iomanip>
using namespace std;
struct rode {
	int x;
	int y;
	int z;
}rode[100][100];//记录过程矩阵中的路线,由点x出发,先经过点k,再经过集合z,过程矩阵j[i][j]中的j就代表集合
int main()
{

	int n, i, j, k, m = 1;
	cout << "顶点个数:";
	cin >> n;
	for (i = 1; i < n; i++)//n个顶点有m个子集,m=2^(n-1)
		m = m * 2;
	//创建动态数组,节省空间
	int **arc = new int*[n];//图的代价矩阵
	for (i = 0; i < n; i++)
		arc[i] = new int[n];
	int **d = new int*[n];//存放迭代结果,即过程矩阵,过程表
	for (i = 0; i < n; i++)
		d[i] = new int[m];
	//输入图的代价矩阵
	cout << "请以矩阵形式输入顶点之间的距离" << endl;
	for (i = 0; i < n; i++)
		for (j = 0; j < n; j++)
			cin >> arc[i][j];
	//纠正用户输入的数据
	for (i = 0; i < n; i++)
		arc[i][i] = -1;
	cout << "您输入的顶点之间的距离如下" << endl;
	for (i = 0; i < n; i++)
	{
		for (j = 0; j < n; j++)
			cout << setw(3) << arc[i][j];
		cout << endl;
	}
	//初始化第0列
	for (i = 0; i < n; i++)
		d[i][0] = arc[i][0];
	//填过程矩阵,第一行,因为第一行代表从顶点0开始经过一些顶点再回到0,但是我们只需要d[0][m-1]这一个值,所以第一行先不计算,等最后再只计算d[0][m-1],节省时间。
	for (j = 1; j < m; j++)//j就代表m个子集,如j=5(二进制为101)代表{1,3},j=3(二进制为011)代表{1,2}。二进制1011就代表集合{1,2,4}。这是设定的一种规则。
	{
		for (i = 1; i < n; i++)
		{
			d[i][j] = 0x7ffff;//设0x7ffff为无穷大
			if (((j >> (i - 1)) & 1) == 1)	//对于数字x,要看它的第i位是不是1,通过判断布尔表达式 (((x >> (i - 1) ) & 1) == 1的真值来实现
				continue;					//若第i位是1,就说明子集j里包含i这个元素,d[i][j]这个空就无需计算
			for (k = 1; k < n; k++)
			{
				/*找出集合j中有哪些元素,比如,(6>>(2-1))&1==1,说明集合{2.3}中有元素2。之所以这么做,
				是因为我们人知道j=6(二进制110)代表集合{2.3},但是计算机不知道,所以要有找元素这一步*/
				if (((j >> (k - 1)) & 1) == 0)	//集合中没有此元素就跳过
					continue;
				/*以d[2][5]为例,d[2][5]=d(2,{1,3})。d[2][5] 表示从2出发,通过{1,3},最后回到起点。
				那么d[2][5] = min{arc[2][1] + d(1,{3}),arc[2][3] + d(3,{1})} = min{arc[2][1]  + d[1][4],arc[2][3] + d[3][1]}。
				从2出发,要去{1,3},先考虑去1的路。去了1后集合{1,3}中只剩下{3} ,{3}对应二进制100,十进制4,所以要求的d表就是d[1][4],这个4可以通过(101)^(1)得到,而(1) = 1<<(1-1).其中,二进制101(十进制5)代表集合{1,3};
				再看去3的路,去了3后集合{1,3}中只剩下{1},{1}对应二进制001,十进制1,所以要求的d表就是d[3][1],1通过(101) ^ (100)得到,而(100) = 1<<(3-1)。*/
				if (d[i][j] > arc[i][k] + d[k][j ^ (1 << (k - 1))])
				{
					d[i][j] = arc[i][k] + d[k][j ^ (1 << (k - 1))];
					rode[i][j].x = i;
					rode[i][j].y = k;
					rode[i][j].z = j ^ (1 << (k - 1));//由x出发,先经过k,再经过集合z
				}
			}
		}
	}
	//计算d[0][m-1],d(0,{1,2,3})=min{arc[0][1]+d(1,{2,3}),arc[0][2]+d(2,{1,3}),arc[2][3]+d(3,{1,2})}
	for (j = 0; j < m - 1; j++)
		d[0][j] = -1;
	d[0][m - 1] = 0x7ffff;
	for (k = 1; k < n; k++)
	{
		
		if ((( m-1 >> (k - 1)) & 1 ) == 0)	
			continue;
		if (d[0][m - 1] > arc[0][k] + d[k][(m - 1) ^ (1 << (k - 1))])
		{
			d[0][m - 1] = arc[0][k] + d[k][(m - 1) ^ (1 << (k - 1))];
			rode[0][m - 1].x = 0;
			rode[0][m - 1].y = k;
			rode[0][m - 1].z = (m - 1) ^ (1 << (k - 1));
		}
	}
	cout << "最短路径为:" << d[0][m-1] << endl;
	//输出路线
	j = m - 1;
	cout << "0→";
	for (i=0;i<n-1;i++)
	{
		cout << rode[i][j].y << "→";
		j = rode[i][j].z;
	}
	cout<< rode[i][j].y << endl;
	//输出过程矩阵
	cout << "过程矩阵为:" << endl;
	cout << '\t';
	for (j = 0; j < m; j++)
		cout << j << '\t';
	cout << endl;
	for (i = 0; i < n; i++)
	{
		cout << i << '\t';
		for (j = 0; j < m; j++)
		{
			if (d[i][j] == 0x7ffff)
				d[i][j] = -1;
			cout << d[i][j] << '\t';
		}
		cout << endl;
	}
	return 0;
}

五、结果截图

在这里插入图片描述

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值