编写参考:https://www.cnblogs.com/ranjiewen/p/6084052.html
【问题描述】
有n个城市坐标,问你从任意一个城市出发经过所有城市后再回到原城市且每个城市仅旅行一次的最短距离是多少
【求解思路】
这是一个经典的完全NP问题,对于此类问题,目前并没有任何高效算法能求得此问题的最优解,而只能通过模拟退火或者是遗传算法求得近似解
本文采用模拟退火求解该问题
【代码实现】
#pragma GCC optimize(2)
#include <time.h>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 1010 //最大城市数
#define eps 1e-8 //结束温度
#define rate 0.98 //温度下降速率
#define length 1000 //迭代次数
#define INF 0x3f3f3f3f //近似无穷大
using namespace std;
int n; //城市数目
double T = 100000 / 1.0; //初始温度
struct path
{
int ans[N]; //保存路径
double len; //路径总长
path()
{
ans[0] = 0;
for (int i = 1; i < N; ++i)
{
ans[i] = 0;
}
len = INF;
}
void operator = (const path& b)
{
for (int i = 0; i < n; ++i)
{
this->ans[i] = b.ans[i];
}
this->len = b.len;
}
}bestpath, ans;
struct point
{
double x, y;
point()
{
x = y = 0;
}
};
point p[N];
double d[N][N]; //存放城市之间的路程
void swap(int& x, int& y)
{
if(x == y) return;
x = x ^ y;
y = x ^ y;
x = x ^ y;
}
double cal(point a, point b) //根据坐标计算路径
{
return sqrt((1.0 * (a.x - b.x) * (a.x - b.x) + 1.0 * (a.y - b.y) * (a.y - b.y)));
}
void getdis()
{
for (int i = 0; i < n - 1; ++i)
{
for (int j = i + 1; j < n; ++j)
{
d[i][j] = d[j][i] = cal(p[i], p[j]);
}
}
}
double Rand() //产生小于1的随机数
{
return (((double)rand()) / (RAND_MAX + 1.0));
}
path getnext(path pp) //随机交换路径中的两个点寻找最优解
{
path temp = pp;
int x = 0, y = 0;
while (x == y)
{
x = (int)(n * Rand());
y = (int)(n * Rand());
}
swap(temp.ans[x], temp.ans[y]);
temp.len = 0;
for (int i = 0; i < n; ++i)
{
if (i != n - 1)
temp.len += d[temp.ans[i]][temp.ans[i + 1]];
}
temp.len += d[temp.ans[n - 1]][temp.ans[0]];
return temp;
}
void print() //打印最终最优路径
{
printf("The approximate solution is:\n");
for (int i = 0; i < n; ++i)
{
printf("%d->", ans.ans[i]);
}
printf("%d\n", ans.ans[0]);
printf("The length of the solution is: %.6f\n", ans.len);
}
void SAA()
{
getdis(); //首先预处理处所有城市间的路径
for (int i = 0; i < n; ++i) //答案初始化
{
bestpath.ans[i] = i;
if (i != n - 1)
{
bestpath.len += d[i][i + 1];
}
}
bestpath.len += d[n - 1][0];
path newpath = bestpath;
while (T > eps) //从初始温度开始进行降温
{
for (int i = 1; i <= length; ++i)
{
newpath = getnext(bestpath); //产生新路径
double delta = bestpath.len - newpath.len;
if (delta > 0.0) //如果当前新求解的路径小于最优路径,更新最优路径
{
bestpath = newpath;
if (ans.len > bestpath.len) ans = bestpath; //ans由于保存所有产生路径中最短的那条
}
else if (exp(delta / T) > Rand()) //否则以一定概率接受差解
{
bestpath = newpath;
}
}
T *= rate; //降温
}
print();
}
int main()
{
srand((unsigned)time(NULL)); //使用真随机数
scanf_s("%d", &n);
if (n > N)
{
printf_s("so large size!\n");
exit(0);
}
for (int i = 0; i < n; ++i)
{
scanf_s("%lf%lf", &p[i].x, &p[i].y);
}
SAA();
return 0;
}
【演示数据】
样例解释
第一行输入总城市数n,随后的n行包含编号0 - (n - 1)所有城市的坐标
样例1:
6
20 80
16 84
23 66
62 90
11 9
35 28
样例2:
27
41 94
37 84
53 67
25 62
7 64
2 99
68 58
71 44
54 62
83 69
64 60
18 54
22 60
83 46
91 38
25 38
24 42
58 69
71 71
74 78
87 76
18 40
13 40
82 7
62 32
58 35
45 21
【演示文件】
样例1
样例2
【基于枚举算法的算法正确性验证】
#pragma GCC optimize(2)
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define INF 0x7fffffff
using namespace std;
const int maxn = 1010;
struct point
{
double x, y;
}a[maxn];
int b[maxn]; //用于记录所有路径的全排列
double dis(point a, point b)
{
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
int main()
{
int n;
scanf_s("%d", &n);
for (int i = 0; i < n; i++)
{
scanf_s("%lf%lf", &a[i].x, &a[i].y);
}
for (int i = 0; i < n; i++)
{
b[i] = i;
}
double ans = INF / 1.0;
do
{
double t = 0.0;
for (int i = 0; i < n; i++)
{
if (i != n - 1)
{
t += dis(a[b[i]], a[b[i + 1]]);
if (t >= ans) //剪枝,距离大于当前解跳出
break;
}
}
t += dis(a[b[n - 1]], a[b[0]]);
ans = min(ans, t);
} while (next_permutation(b, b + n)); //全排列
printf_s("The accurately anser is: %lf\n", ans);
return 0;
}
演示结果
样例1
样例2
(电脑跑不动)
可以看到,SAA所产生的近似解与最优解相差不大
【结果分析】
样例1中,SAA与搜索出来的答案几近相同,但时间上比搜索略慢,说明在点数较少时,初始温度过高可能会导致运行的时间加长,并且点数较少时答案较为可靠。
而在样例2中,搜索的运行时间远远高于模拟退火(搜索复杂度O(n!)),时间上搜索无法接受,而SAA却可在极短时间内产生近似最优解,再次证明了SAA算法的优点所在。
【后记】
目前本人尚不清楚此问题的初始温度及降温速率如何设置比较合理