目录
- 蛮力法
- TSP 问题
- 实验程序编写
- 图结构体定义
- 城市拓扑的建立
- DFS
- 主函数
- 获取实验数据
- 实验数据分析
- 路线数
- DFS 次数
- 数据文件大小
- 总结
蛮力法
蛮力法也称穷举法或枚举法,是一种简单直接地解决问题的方法,常常直接基于问题的描述,所以蛮力法也是最容易应用的方法。蛮力法所依赖的基本技术是遍历,也称扫描,即采用一定的策略依次理待求解问题的所有元素,从而找出问题的解。依次处理所有元素是蛮力法的关键,为了避免陷人重复试探,应保证处理过的元素不再被处理。
TSP 问题
TSP 问题(Traveling Salesman Problem)又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访 n 个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。
使用蛮力法求解 TSP 问题的思想是,通过穷举的方式把所有可能的路径找出来,然后对每一条路径都计算开销,最终找出开销最小的路径。例如对于如图 4 个城市的拓扑,使用蛮力法求解的过程如表格所示。
序号 | 路径 | 路径长度 | 是否最短 |
1 | a->b->c->d->a | 18 | 否 |
2 | a->b->d->c->a | 11 | 是 |
3 | a->c->b->d->a | 23 | 否 |
4 | a->c->d->b->a | 11 | 是 |
5 | a->d->b->c->a | 23 | 否 |
6 | a->d->c->b->a | 18 | 否 |
当城市规模增大时,存在的路径数会呈现指数型增长,例如 11 个城市的拓扑图如下所示。
实验程序编写
图结构体定义
TSP 是个 NP 完全问题,我们需要图结构来进行存储。我选择邻接矩阵存储城市拓扑图,定义的图结构体如下。
typedef struct //图的定义
{
int edges[MAXV][MAXV]; //邻接矩阵
int n; //顶点数
} MGraph;
城市拓扑的建立
接下来就需要把城市拓扑存储在邻接矩阵中,因为城市拓扑是完全图,因此我们需要存储所有城市之间的距离。
MGraph CreateMGraph(int num) //建图
{
MGraph topography;
for (int i = 1; i <= num; i++)
{
for (int j = 1; j <= num; j++)
{
topography.edges[i][j] = 0;
}
}
for (int i = 1; i <= num; i++)
{
for (int j = i + 1; j <= num; j++)
{
printf("城市%d和城市%d之间的距离为:",i,j);
cin >> topography.edges[i][j];
topography.edges[j][i] = topography.edges[i][j];
}
}
topography.n = num;
return topography;
}
DFS
想要获取最短的路线,使用蛮力法进行分析时需要先获取所有的路径。DFS 可以获取所有的路径,编写的代码如下。注意当获取一条新路径时,需要先把该路径拷贝到下一个路径,因为递归实现的 DFS 无法返回上一层递归执行填充操作。这么做是可行的,因为相邻路径不需要回溯的路线是一样的,而回溯的部分会直接覆盖原来的路线。
void DFS(int new_point, int cities_visited, int &path_index) //深度遍历
{
count++;
if (cities_visited == topography.n) //所有城市都走一遍
{
path[path_index][cities_visited] = new_point;
path[path_index][cities_visited + 1] = start_point; //回到出发点
for(int i = 1; i <= topography.n; i++)
{
path[path_index + 1][i] = path[path_index][i]; //下一条路径拷贝上一条
}
path_index++;
}
else
{
for (int i = 1; i <= topography.n; i++)
{
if (visited[i] == 0)
{
visited[i] = 1;
path[path_index][cities_visited] = new_point;
DFS(i, cities_visited + 1, path_index);
visited[i] = 0; //回溯到上一城市
}
}
}
return;
}
主函数
接下来要计算所有路径的长度,并且得出最短的路线。同时我们也需要确定 DFS 执行了多少次,方便我们分析时间复杂度。
int main()
{
int cities_num = 0; //城市数量
int path_num = 1; //路径数
int cities_visited = 1; //已访问城市数
int path_index = 1; //已获取的路径数
int min_path = 0;
int min_sum = 9999999;
int sum;
cout << "城市数量为:";
cin >> cities_num;
//建图
topography = CreateMGraph(cities_num);
for(int i = cities_num - 1; i > 1; i--)
{
path_num *= i;
}
//初始化访问状态
for(int i = 1; i <= topography.n; i++)
{
visited[i] = 0;
}
//出发
cout << "从哪个城市出发:";
cin >> start_point;
visited[start_point] = 1;
//获取所有路径
DFS(start_point, cities_visited, path_index);
//得出最短路径
ofstream outfile;
outfile.open("11.txt");
for (int i = 1; i < path_index; i++)
{
sum = 0;
outfile << "路径" << i << ":";
for (int j = 1; j <= cities_num; j++)
{
sum += topography.edges[ path[i][j] ][ path[i][j + 1] ];
}
if(sum < min_sum)
{
min_sum = sum;
min_path = i;
}
}
cout << "\n最短路径为路径" << min_path << ":";
for (int j = 1; j <= cities_num; j++)
{
cout << path[min_path][j] << " -> ";
outfile << path[min_path][j] << " -> ";
sum += topography.edges[ path[min_path][j] ][ path[min_path][j + 1] ];
}
cout << path[min_path][cities_num + 1] << endl;
cout << "最短路径长度为:" << min_sum << endl;
cout << "DFS 次数为:" << count;
return 0;
}
获取实验数据
使用上述不同规模的城市拓扑分析TSP问题,得出的实验数据如下。
城市规模(个) | 路线数(条) | DFS次数(次) | 数据文件大小(KB) |
4 | 6 | 16 | 1 |
5 | 24 | 65 | 2 |
6 | 120 | 326 | 7 |
7 | 720 | 1957 | 45 |
8 | 5070 | 13700 | 348 |
9 | 40320 | 109601 | 3050 |
10 | 362880 | 986410 | 30260 |
11 | 3628800 | 9864101 | 330943 |
实验数据分析
当使用蛮力法解决TSP问题时,需要考虑从某个城市出发的所有路线。由于城市之间彼此互通,城市拓扑是个完全图,因此所有路线的数量规模是 n-1 个城市的全排列。
当输入城市数量n时,会产生n!条路线,从而计算路径长度的操作就需要执行n!次。也就是说蛮力法解决TSP 问题的 T(n) = n!,从而得出 O(n) = n!。无论是路线数、DFS 次数还是数据文件大小,都能明显地体现这个趋势。
路线数
DFS 次数
数据文件大小
总结
我一开始使用的是 C++ 的 new 运算符动态内存分配二维数组。但是除了城市规模 4 的数据下,其他的规模均无法正常运行,并且主函数 “return value 3221225477”。经过查阅资料得知这可能和未初始化的变量或指针引发的,但是我并不知道问题代码及其原因,无奈之下只好直接定义了一个较大的二维数组进行存储。
在测试城市规模 12 的数据时,由于栈区空间已经用尽,我打算使用动态内存分配使用堆区内存。结果需要分配的内存过多,导致所有内存空间全部被 C++ 占用,而 C++ 并没有智能保护内存的机制,导致我的电脑直接宕机。在强行断电并修复电脑之后觉定放弃 12 个城市的 TSP 问题求解。
蛮力法是解决问题明确而直接的手法,程序编写较为简单,思路是模拟情景下的所有可能性进行分析,在时间允许下是极佳的算法。但是在不同的情景下会存在效率低下的情况,我们会需要更加巧妙的算法提高解决问题的效率,期待接下来对算法的进一步学习,使用其他的算法对这些问题进行求解。