Travelling Salesman Problem
旅行商问题,即TSP问题(Travelling Salesman Problem)又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。
旅行商问题是图论中最著名的问题之一,即“已给一个n个点的完全图,每条边都有一个长度,求总长度最短的经过每个顶点正好一次的封闭回路”。该问题通常被认为是一个NP完全问题。时间复杂度为O(n!)。因此,通常n的值不是很大。
因此我们如何求其近似解(非多项式时间算法)呢?这里使用动态规划
- 假定我们从城市1出发,经过了一些地方,并到达了城市j。毋庸置疑,我们需要记录的信息有当前的城市j。同时我们还需要记录已经走过的城市的集合。同理,使用S记录未走过的城市的集合也可以的,且运算方便。
- 于是我们可以得出状态转移方程 go(S,init)=min{go(S−i,i)+dp[i][init]}∀s∈S go(s,init)表示从init点开始,要经过s集合中的所有点的距离
- 因为是NP问题,所以时间复杂度通常比较大。使用dis[s][init]用来去重,初始化为-1,如果已经计算过init—>S(递归形式),则直接返回即可
例: Sicily1000. Traveling Salesman Problem
题目大意:有编号1到N的N个城市,问从1号城市出发,遍历完所有的城市并最后停留在N号城市的最短路径长度。N<=20
那么如何有效地储存当前还未走过的城市集合S呢?使用一个整形即可。每个整形可以表示32位,即32个城市,用1代表当前未去,0代表当前已经访问过的点。因N<=20,所以S<=1048576.
代码如下:*
#include<iostream>
#include<cmath>
#include<iomanip>
using namespace std;
int s;
int N;//点的个数
int init_point;
double x[20];
double y[20];
double dp[20][20];//两个城市的距离
double dis[1048577][20];//2^20=1048576 表示出发点到S集合是否已经访问过
double go(int s,int init)
{
if(dis[s][init]!=-1) return dis[s][init];//去重
if(s==(1<<(N-1))) return dp[N-1][init];//只有最后一个点返回
double minlen=100000;
for(int i=0;i<N-1;i++)//只能在1到n-2点中查找
{
if(s&(1<<i))//如果i点在s中且不为发出点
{
if(go(s&(~(1<<i)),i)+dp[i][init]<minlen)
minlen=go(s&(~(1<<i)),i)+dp[i][init];
}
}
return dis[s][init]=minlen;
}
int main()
{
int T;
cin>>T;
while(T--)//测试样例数
{
cin>>N;
for(int i=0;i<N;i++)
cin>>x[i]>>y[i];//读入城市的坐标
for(int i=0;i<N;i++)
for(int j=0;j<N;j++)
{
dp[i][j]=sqrt(pow((x[i]-x[j]),2)+pow((y[i]-y[j]),2));
//计算两个城市的距离
}
for(int i=0;i<pow(2,N);i++)
for(int j=0;j<N;j++)
dis[i][j]=-1;//去重数组初始化
init_point=0;
s=0;
for(int i=1;i<N;i++)
s=s|(1<<i);//从1开始,保证初始点没有在S里面
double distance=go(s,init_point);
cout<<fixed<<setprecision(2)<<distance<<endl;
}
}