旅行商问题 TSP (Traveling Salesman Problem)又称旅行推销员问题是指对于给 定的 n 个城市旅行商从某一城市出发不重复地访问其余每一城市后回到出发的城市 寻找一条使总旅行路程最短的路线。
一、基本贪心法
假设给定一个带权无向图G,G = {V, E},顶点集V = {0,1,2,3,4}, 边集E = {(0,1), (0,2), (0,4), (1,2), (1,3), (3,4)},如图所示。每一条边带有权重,表示两点之间的距离。旅行商问题就是要找到遍历所有节点的序列,同时满足遍历路径长度最短。
在如上所示的图中,假设起始点为0点,满足旅行商问题的解有两条,0-1-2-3-4或0-2-1-3-4两条路径,最短路径长度之和为16。求解旅行商问题的方法有很多,本文主要介绍基于贪心法如何求解旅行商问题。

基本贪心法存在一个问题,就是并不是所有的输入数据都能找到可行的解。这里说的是可行的解,而不是最优解。比如,以上图为例,假设起始点设置在0点,从0出发,与0相连的节点有三个,其中与0相连的距离最近的未访问的节点是2,于是访问节点2,添加节点2到已访问数组,接着以2作为起点继续上述过程。继续这样操作下去,最终就会发现,节点1被略过了没有被访问。所以,基本贪心法不是一定能找到一条可行的解。
基本贪心算法求解旅行商问题
算法过程描述
假设有N个城市,城市用数字编号来表示,0,1,2,3,...,N-1,任意两个城市的距离记录在距离数组D[N][N],被访问过的城市被记录在已访问数组S中,其中S[i]表示第i个已被访问的城市。
基本贪心法求解TSP算法步骤:
- 初始化距离数组D,已访问数组S;
- 设置起始城市start已被访问;
- 如果还有没有访问的城市
- 找到start到其他没有被访问城市中距离最短的城市next_city;
- 加入next_city到已访问数组中;
- 更新start为next_city;
- 累加当前路径和;
- 当所有城市全被访问后
- 输出遍历序列
- 算法结束
代码用C++语言实现
#include <iostream>
using namespace std;
#define MAX 1000000
#define n 5
int D[n][n]; //记录两城市间距离
int S[n]; //记录第i次访问的城市
/**
* 初始化距离数组和访问数组
*/
void Init() {
for (int i = 0; i < n; ++i)
for (int j = i; j < n; ++j)
D[i][j] = D[j][i] = MAX;
for (int i = 0; i < n; ++i)
S[i] = -1;
}
bool checkAccess(int city) {
//判断该城市是否访问过
for (int i = 0; i < n; ++i) {
if (S[i] == city)
return true;
}
return false;
}
int findCity(int city) {
//查找下一次要去的距离最短的城市
int min = MAX;
int nextCity = -1;
for (int i = 0; i < n; ++i) {
if (D[city][i] < min && checkAccess(i) == false) {
min = D[city][i];
nextCity = i;
}
}
return nextCity;
}
void TSP(int start) {
S[0] = start;
int sum = 0; // 路程总和
int i = 1;
while (true) {
S[i] = findCity(S[i - 1]);
if (S[i] == -1) break; // 如果没有找到下一个要访问的城市,即S[i] == -1,跳出循环
else {
sum += D[S[i - 1]][S[i]];
}
i += 1;
}
printf("访问顺序为:\n");
int j = 0;
while (S[j] != -1) {
printf("%d", S[j]);
j += 1;
}
printf("总距离为:%d", sum);
}
int main() {
Init();
D[0][1] = D[1][0] = 4;
D[0][2] = D[2][0] = 3;
D[0][4] = D[4][0] = 5;
D[1][3] = D[3][1] = 2;
D[1][2] = D[2][1] = 5;
D[2][3] = D[3][2] = 1;
D[3][4] = D[4][3] = 1;
TSP(0);
return 0;
}
访问顺序为: 0234 总距离为:5
对于示例1,图的遍历顺序为0234。从输出结果中可以看出,这不是一个满足旅行商问题要求的解,因为在该解中,没有访问1号城市。所以,可以看出,这种贪心法是存在问题的,现在得到的解是否是全局最优解的问题,而是根本就不满足题目要求。所以基本贪心算法仍需要进一步改进。
二、改进贪心法求解旅行商问题
改进的贪心法从起始点开始,以一定的方式逐渐将解序列扩充为完整的解序列。扩充序列时,可以通过换位、移位和倒位三种基本次序的变换操作,改变原来解序列的排列次序,得到新的解序列,如果该解序列表示的路径更短,则保留该路径。以这种方式不断扩充解序列,直到所有的节点都被访问后,算法结束。
改进贪心算法过程描述:
对城市进行编号,0,1,2,...,n,假设起始点为0。设置城市距离数组D,设置已访问过城市序列R和未访问城市序列S。
- 初始化城市距离数组D;
- 按照邻接矩阵填充城市距离数组D;
- 构造解序列S,加入起始点0;
- 如果已访问城市数组还没有满,则
- 从未访问解序列之外的城市中选择一个新城市k,插入到原有的解序列Tk-1 = {c1, c2, ..., ck-1}中,得到新的部分解序列Tk = {c1, c2, ..., ck, ..., ck-1}。该过程由贪心法决定。
- 否则,即已访问城市已满,算法结束
C++代码实现
#include <iostream>
#include <vector>
#define n 5
#define MAX 10000
using namespace std;
int D[n][n]; // 城市距离数组
vector<int> S; // 已经访问过的城市序列,即当前解集
vector<int> R; // 还没有访问过的城市序列
void Init() {
for (int i = 0; i < n; i++) {
for (int j = 0;j < n; j++) {
D[i][j] = D[j][i] = MAX;
}
}
// 初始化未访问数组
for (int k = 0; k < n; k++) {
R.push_back(k);
}
}
/**
* 计算已访问城市的路径长度
* int result: 路径长度
*/
int path_sum(vector<int> path) {
int length = path.size();
int result = 0;
for (int i = 0; i < length-1; i++) {
result += D[path[i]][path[i+1]];
}
return result;
}
/**
* 获得未访问的城市个数
*/
int unvisited_length(vector<int> R) {
int result = 0;
for (int i = 0; i < (int)R.size(); i++) {
if (R[i] != -1) {
result += 1;
}
}
return result;
}
int getIndex(int city) {
int index = -1;
for (int i = 0; i < (int)R.size(); i++) {
if (R[i] == city) {
return i;
}
}
return index;
}
void TSP(int start) {
R.erase(R.begin() + start); // 删除起始节点
S.push_back(start); // 添加起始节点已访问
int visited_citys = S.size();
int unvisited_citys = unvisited_length(R);
while (visited_citys != n) {
int insert_city = 0;
int insert_index = 0;
int min_sum = MAX;
unvisited_citys = unvisited_length(R);
// 找到剩下某一个未访问城市的最佳插入位置
int candidate = 0;
while (unvisited_citys != 0) {
// 找到下一个候选城市
int city = R[candidate];
int sum = MAX;
int index = 0;
int position = 1;
// 找到当前候选城市的最佳插入位置
while (position <= visited_citys) {
// 插入到已访问城市序列
S.insert(S.begin()+ position, city);
// 评估当前城市序列的游路长度
int new_sum = path_sum(S);
if (new_sum < sum) {
sum = new_sum;
index = position;
}
S.erase(S.begin() + position);
position += 1;
}
if (sum < min_sum) {
min_sum = sum;
insert_city = city;
insert_index = index;
}
unvisited_citys -= 1;
candidate += 1;
}
// 找到下一个城市的插入位置
S.insert(S.begin() + insert_index, insert_city);
R.erase(R.begin() + getIndex(insert_city));
visited_citys += 1;
}
}
void test() {
// vector<int> vec {0, 2, 3, 4};
vector<int> vec {0, 1, 3, 4};
// int sum = path_sum(vec); // check
// cout << sum << endl;
int citys = unvisited_length(vec); // check
cout << citys << endl;
}
int main() {
Init();
D[0][1] = D[1][0] = 4;
D[0][2] = D[2][0] = 3;
D[0][4] = D[4][0] = 5;
D[1][3] = D[3][1] = 2;
D[1][2] = D[2][1] = 5; // 插入边时漏写了一条
D[2][3] = D[3][2] = 1;
D[3][4] = D[4][3] = 1;
// test();
TSP(0);
cout << "访问序列:" << endl;
for (int i = 0; i < (int)S.size(); i++) {
cout << S[i] << " ";
}
return 0;
}
访问序列: 0 1 2 3 4 路径长度为:11
参考资料
[1]潘立登,黄晓峰.用启发式贪心法求解旅行商问题[J].北京化工大学学报(自然科学版),1998(02):48-53.


本文介绍了旅行商问题(TSP),并探讨了基于贪心算法的基本方法及其存在的问题。通过示例展示了贪心算法无法保证找到满足问题要求的解。接着,提出了改进贪心算法,通过变换操作不断优化解序列,以期获得更短路径。最后,给出了C++代码实现,并引用了相关参考资料。
2236

被折叠的 条评论
为什么被折叠?



