计算机科学与工程学院实验报告
课程名称 | 算法设计与分析 | 班级 | ||||||
实验内容 | 实验3:TSP问题 | 指导教师 | ||||||
姓名 | 重剑DS | 学号 | 实验日期 | 2022.06.06 |
一、问题描述,含输入、输出数据内容、格式
前言:
旅行推销员问题(英语:Travelling salesman problem, TSP)是这样一个问题:给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短回路。它是组合优化中的一个NP难问题,在运筹学和理论计算机科学中非常重要。
很多方法本来是从TSP发展起来的.后来推广到其他NP完全问题上去。[1]
由于其在交通运输、电路板线路设计以及物流配送等领域内有着广泛的应用,国内外学者对其进行了大量的研究。早期的研究者使用精确算法求解该问题,常用的方法包括:分枝定界法、线性规划法、动态规划法等。但是,随着问题规模的增大,精确算法将变得无能为力,因此,在后来的研究中,国内外学者重点使用近似算法或启发式算法,主要有遗传算法、模拟退火法、蚁群算法、禁忌搜索算法、贪婪算法和神经网络等。[2]
从图论的角度来看,该问题实质是在一个带权完全无向图中,找一个权值最小的Hamilton回路。由于该问题的可行解是所有顶点的全排列,随着顶点数的增加,会产生组合爆炸,它是一个NP完全问题。
问题描述:
给定一个无向图,试着找出一个权值最小的能够从指定点出发并且一次经过所有结点并回到出发点的路径,如果存在该路径,输出该路径和总权值,不存在该路径的话输出“不存在这样一条路径”。
输入数据格式:
第一行输入总的结点数n,顶点编号为1,2,…,n。
第二行输入总的边数m。
接下来按照这样的输入格式:“顶点1 顶点2 边权”,每行输入一条边的信息。
最后一行输入起点的结点编号。
【输入样例】
5
9
1 2 10
1 5 12
1 4 4
2 5 5
5 4 6
2 4 8
5 3 30
2 3 15
4 3 7
1
输出数据格式:
程序运行结束时,将计算出的结果输出。若存在路径,则输出具体的路径信息和路径权值,不存在路径则输出“不存在这样一条路径”。
【输出样例】
最短长度:43
最短路径为:1->4->3->2->5->1
二、调试环境介绍
IDE:Dev-C++ 5.4.0
编程语言:C++
三、调试数据及结果(包含指定的调试数据及结果),有调试界面截图
调试的输入数据:
3
3
1 2 1
3 1 3
2 3 2
1
调试输入数据的图可视化后如下图1所示:
图1
上机输入效果界面截图:
运行结果:
最终运行界面截图:
中间运行过程debug调试截图:
DP表的打印,行从1~3,列从0111(表集合)
由上图可以看到,D[1][0]为0,因为从1出发再到location(1)的花费为0(相当于自己到自己)
D[2][0]就是从2出发,不经过任何城市,回到location的花费,所以D[2][0] = graph[3][location]
由上表可知D[2][100(2)相当于{3}]=5,
状态转移: D[1][ 110(2)相当于{2,3}] = min{ D[2][{3}] + graph[1][2] = 5 + 1 , D[3][{2}] + graph[1][3] = 3 + 3 } = 6
- 算法流程图、算法的时间复杂度和空间复杂度
算法流程图如下图所示:
算法的时间复杂度如下:
在本代码中,主要的时间花费是在如下图所示的三重循环上。
每个for循环相当于一个乘法,第一重遍历所有集合状态大概有2n,第二重循环枚举出发结点i大概有n,最内层的循环枚举顶点集j中的结点k大概有n,那么总的时间复杂度大概就是O(n2*2n),是指数级别的时间复杂度,随着给的结点数n的增大,花费的时间将会成指数级爆炸趋势。问题规模和花费时间的关系图大体如下图所示:
算法空间复杂度分析如下:
算法的空间复杂度主要来自用于存储状态的D二维数组和存储结果路径的Rec二维数组。存图所用的邻接矩阵graph (二维数组,大概n行n列)在它们面前显得会很小,所以在问题规模n足够大时可以近似忽略。D和Rec的列数为模拟集合的个数,大概有2n,行数大概为输入所给的结点数n,所以它们所要使用的存储空间所反映的空间复杂度为O(n2*2n)。问题数据规模和所使用的存储空间大小的关系图大体如下图所示:
五、实验中遇到的问题及解决方案(10-50字)
在实验过程中,貌似出现了初始化数组时所使用的初始值不同所导致的问题,调式的时候看得有点懵。后面统一初始化为inf ,调式的时候就显得更清晰明白一点了。
六、参考文献(2-5个)
[1] 郭靖扬. 旅行商问题概述[J]. 大众科技, 2006(8):229-230.
[2] 陈文兰, 戴树贵. 旅行商问题算法研究综述[J]. 滁州学院学报, 2006, 8(3):1-6.
附:详细代码
#include<iostream>
#include<cmath>
using namespace std;
const int inf = 0x3f3f3f3f;
//TSP问题求解函数
void TSP(int n, int** graph, int location){
//构建二维数组[i,2^n],j模拟集合个数
//D[i][j]=x表示从 i出发,经过j集合(二进制下110表示经过2、3结点)中所有结点一次,然后回到 location 的最小花费x
//那么j集合必然不包含起始点location,因为最后也要返回到location
//j集合也不包含i,因为就是从i结点出发的
int **D = new int*[n+1]; //建行0~n,为方便理解和表示,第0行未使用
int **Rec = new int*[n+1]; //建立Rec 存放决策,记录前驱节点
for(int i = 1; i <= n; i++) {
//建列表示集合
D[i] = new int [(int)pow(2,n)];
Rec[i] = new int [(int)pow(2,n)];
}
//初始化D、Rec
for(int i = 1; i <= n; i++){
//dp[3][{}]就是从3出发,不经过任何城市,回到location的花费,所以dp[3][{}] = graph[3][location]
D[i][0] = graph[i][location]; //D[i,{}]
cout << D[i][0] << endl;
Rec[i][0] = -1;
for(int j = 1; j <= (int)pow(2, n)-1; j++){
D[i][j] = inf;
Rec[i][j] = -1;
}
}
cout << endl;
//动态规划
for(int j = 1; j <= (int)pow(2, n)-1; j++){ //对每一列求解
for(int i = 1; i <= n; i++){ //每一行找最短路径
int min = inf;
//顶点集j不能包含i(编号为2^(i-1))
if(((int)pow(2, i-1) & j) == 0){
int length = inf;
for(int k = 1; k <= n; k++) {
//若顶点k(编号为2^(k-1))在顶点集j中
if((int)pow(2, k-1) & j ) {
length = graph[i][k] + D[k][j-(int)pow(2,k-1)];
if(length < min) {
min = length;
D[i][j] = min;
Rec[i][j] = k; //局部最优决策
}
}
}
}
}
}
if (D[location][(int)pow(2,n)-1 - (int)pow(2,location-1)] >= inf) {
cout << "不存在这样一条路径\n";
return;
}
//调试 打印dp表用
cout << "DP表:" << endl;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= pow(2,n)-1; j++) {
cout << D[i][j] << " ";
}
cout << endl;
}
cout<<"最短长度:"<<D[location][(int)pow(2,n)-1-(int)pow(2, location-1)]<<endl;//最短路径长度
cout<<"最短路径为:"<<location;
int row = location;
int column = (int)pow(2,n)-1-(int)pow(2,row-1);
while(column > 0) {
cout << "->" << Rec[row][column];
row = Rec[row][column];
column -= (int)pow(2,row-1);
}
cout << "->" << location << endl;
}
int main(){
cout << "旅行家需要游历的城市数目?:" << endl;
int n;
cin >> n;
//建立二维数组模拟邻接矩阵
int **graph = new int* [n+1];
for(int i = 1; i <= n; i++)
graph[i] = new int[n+1];
//初始化边权值
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
graph[i][j] = inf;
//自己到自己的边权为0
for (int i = 1; i <= n; i++) graph[i][i] = 0;
//邻接矩阵输入方法
//for(int i=1;i<=n;i++){
// for(int j=1;j<=n;j++){
// cout<<"输入邻接矩阵graph["<<i<<"]["<<j<<"]的权:"<<endl;
// cin>>graph[i][j];
// graph[t2][t1] = graph[i][j];
// }
//}
cout << "请输入总边数:" << endl;
int m; //m为边数
cin >> m;
cout<<"输入格式: 顶点1 顶点2 边权:"<<endl;
//输入格式: 顶点1 顶点2 边权
for (int i = 0; i < m; i++) {
int t1, t2, cost;
cin>>t1>>t2>>cost;
graph[t1][t2] = graph[t2][t1] = cost;
}
cout << "旅行家出发的城市编号?" << endl;
int location;
cin >> location;
TSP(n, graph, location); //TSP求解
return 0;
}
/*
3
3
1 2 1
3 1 3
2 3 2
1
*/