旅行售货员问题实例
如下图,一个售货员从A城市出发,要到B、C、D、E这几个城市去推销商品,已知各城市之间的路程,问应该如何选定一条从A城市出发,经过每个城市一遍,最后回到城市1的路线,使得总的周游路程最小?
汉密尔顿回路
说白了这就是一个求最短汉密尔顿回路的问题。我们先来了解一下汉密尔顿路径
,汉密尔顿回路
还有汉密尔顿图
- 汉密尔顿路径:
G= (V,E)是一个图,若G中一条路径通过且仅通过每一个顶点一次,称这条路径为哈密顿路径。- 汉密尔顿回路:
若G中一个回路通过且仅通过每一个顶点一次,称这个环为哈密顿回路。- 汉密尔顿图:
若一个图存在哈密顿回路,就称为哈密顿图。
算法分析
我们可以把上面的图转变成树,起点为根结点,每一个从根节点到叶子结点的路径对应图的一个汉密尔顿路径。如下图所示。
接下来就是从A开始走,一条条路径地筛选。先说一下本篇博客最终代码实现主要用到的数据结构:
- 首先我们需要一个邻接矩阵(二维数组)
graph
存储这个图 - 其次需要一个
shortest_length
整型变量存储已经遍历过的回路中的当前最短路径 - 然后需要两个存储路径的数组
path
和best_path
,前者用于存储当前正在筛选的汉密尔顿路径,后者存储遍历完一条路径之后最终的最短路径。
每次到达一个城市就跟把当前已走的路径长度跟目前存储在shortest_lenght
中的值作比较,如果还比最短值小,就在path
中记录下当前的路径(依次填入到达的城市),继续往下走;但如果发现超出最短值不必再走下去,直接回到起点,继续下一个路径(因为在还没形成回路的情况下就已经比当前最短路径的长度长,此时往下走就没有意义了)。
其实这就是回溯算法的基本思想。
回溯算法
- 回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。
- 用回溯算法解决问题的一般步骤为:
- 定义一个解空间,它包含问题的解。
- 利用适于搜索的方法组织解空间。
- 利用深度优先法搜索解空间。
- 利用限界函数避免移动到不可能产生解的子空间。
由于最终要看的是回路的长度,所以我们每次走完一个路径到达树的叶子节点,即最后一层之后,还需要加上该汉密尔顿路径的终点到起点的距离来跟shortest_length
再次比较,不同于之前每次到达一个城市的比较,此处的比较目的是更新short_length
和best_path
的值,看当前路径是否更短,然后回到起点继续走,直至所有路径都走完。
代码实现
下面的代码实现是以上面的例子作为输入,然后求出最短的汉密尔顿回路。
#include<stdio.h>
#define INF -1
/*全局变量*/
int n, //结点数目
length = 0, //临时存储当前路径的路径长
shortest_length = INF, //存储最短路径长
**graph, //存储图
*path, //临时存储路径
*best_path; //存储最短路径
/*交换*/
void swap(int *array, int i, int j)
{
int temp;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
/*结果输出*/
void output()
{
printf("最短路径长度是%d\n", shortest_length);
printf("此时路径为:%d", best_path[1]);
for (int i = 2; i < n + 1; i++)
printf("-->%d", best_path[i]);
printf("-->%d\n", best_path[1]);
}
/*递归回溯*/
void travel(int t)
{
/*终止条件,并处理最后一层*/
if (t == n)
{
if (graph[path[t - 1]][path[t]] != INF
&& graph[path[t]][1] != INF
&& (length + graph[path[t - 1]][path[t]] + graph[path[t]][1] < shortest_length || shortest_length == INF))
{
/*更新路径和最短回路的长度*/
for (int i = 0; i < n + 1; i++)
best_path[i] = path[i];
shortest_length = length + graph[path[t - 1]][path[t]] + graph[path[t]][1];
}
return;
}
/*递归的主体部分*/
for (int i = t; i < n + 1; i++)
{
if (graph[path[t - 1]][path[i]] != INF /*判断是否有路*/
&& (length + graph[path[t - 1]][path[i]] < shortest_length || shortest_length == INF)) /*判断是否比当前最短路径还要长,如果还没形成回路就已经比shortest_length大就直接进入下一轮*/
{
/*通过交换调整最短路径*/
swap(path, i, t);
/*更新length的值,便于下一层递归的筛选*/
length += graph[path[t - 1]][path[t]];
/*进入下一层递归;*/
travel(t + 1);
/*还原*/
length -= graph[path[t - 1]][path[t]];
swap(path, i, t);
}
}
}
int main()
{
/*起点*/
int start;
/*指定结点数目,可以根据需要自行修改*/
n = 5;
/*内存分配*/
graph = (int**)malloc(sizeof(int*) * (n + 1));//为二维数组分配n+1行
for (int i = 0; i <= n; i++)
graph[i] = (int*)malloc(sizeof(int) * (n + 1));
path = (int*)malloc(sizeof(int)*(n + 1));
best_path = (int*)malloc(sizeof(int)*(n + 1));
/*初始化*/
for (int i = 0; i < n + 1; i++)
{
path[i] = i;
for (int j = 0; j < n + 1; j++)
graph[i][j] = INF;
}
/*图输入,可以根据需要自行修改,结点从1开始编号,且编号即下标*/
graph[1][2] = graph[2][1] = 3;
graph[1][5] = graph[5][1] = 9;
graph[1][4] = graph[4][1] = 8;
graph[2][3] = graph[3][2] = 3;
graph[2][4] = graph[4][2] = 10;
graph[2][5] = graph[5][2] = 5;
graph[3][4] = graph[4][3] = 4;
graph[3][5] = graph[5][3] = 2;
graph[4][5] = graph[5][4] = 20;
//--------------------------------------------------------
/*指定起点*/
printf("请输入以第几个结点作为起点:");
scanf("%d", &start);
getchar();//吸收回车
/*调用递归开始遍历路径*/
travel(start+1);
/*结果输出*/
output();
/*释放动态开辟的空间*/
for (int i = 0; i < n + 1; i++)
free(graph[i]);
free(graph);
free(best_path);
free(path);
return 0;
}