旅行商问题
问题描述
假设有5个点,这五个点之间是用无向边来连接的,但是每一个边是有权重的,这实际上是一个无向带权图。我们希望在最小权重的情况下走过这5个点,且不重复,那应该怎样来实现呢?
算法设计
- 定义问题的解空间:问题解的形式为n元组{x1,x2,...,xi,...,xn},分量xi表示第i个要去的旅游景点编号,景点的集合为S={1,2,...,N}。因为景点不可重复走,因此在确定xi时,前面走过的景点{x1,x2,...,xi,...,xi-1}不能再走。xi的取值为S-{x1,x2,...,xi,...,xi-1}。
- 解空间的组织形式:解空间是一颗排列树,树的深度为n=5。除了开始结点1之外,一共有24种排列方式。
- 搜索解空间:约束条件:用二维数组g[][]储存无向带权图的邻接矩阵,如果gi≠无穷表示城市i和城市j又边相连,能走通。限界条件:cl
- 搜索过程:扩展结点沿着某个分支扩展时需要判断约束条件和限界条件,如果满足,则进入深一层继续搜索,如果不满足,则剪掉该分支。搜索到叶子结点时,即找到了当前最优解。搜索直到全部的活结点变成死结点为止。
完美图解
(1)数据结构:如下表所示的邻接矩阵。
3 | 8 | 9 | ||
---|---|---|---|---|
3 | 3 | 10 | 5 | |
3 | 4 | 3 | ||
8 | 10 | 4 | 20 | |
9 | 5 | 3 | 20 |
(图中未填的就是无穷)
(2)初始化:cl=0,bestl=无穷,解分量x[i]和最优解bestx[i]初始化为:
i | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
x[i] | 1 | 2 | 3 | 4 | 5 |
i | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
bestx[i] | 0 | 0 | 0 | 0 | 0 |
(3)开始搜索第一层(t=1)
扩展A0结点,因为我们是从1号结点出发,所以x[1]=1,生成A结点。
(4)扩展A结点(t=2)
沿着x[2]=2分支扩展,因为1号结点和2号结点有边相连,且cl+g1=0+3
(5)扩展B结点(t=3)
沿着x[3]=3分支扩展,因为2号结点和3号结点有边相连,且cl+g2=3+3=6
(6)扩展C结点(t=4)
沿着x[4]=4分支扩展,因为3号结点和4号结点有边相连,且cl+g3=6+4=10
(7)扩展D结点(t=5)
沿着x[5]=5分支扩展,因为4号结点和5号结点有边相连,且cl+g3=10+20=30
(8)扩展E结点(t=6)
t>n,判断5号结点是否和1号结点相连,确认有,且cl+g5=30+9=39
(9)向上回溯到D,D的孩子已经生成完毕,成为死结点,继续回溯到C,C结点还有一个孩子未生成。
(10)重新扩展C结点(t=4)。沿着x[4]=5分支扩展,因为3号结点和5号结点有边相连,且cl+g3=6+3=9
(11)扩展F结点(t=5)。
沿着x[5]=4分支扩展,因为5号结点和4号结点有边相连,且cl+g5=9+20=29
(12)扩展G结点(t=6)。t>n,判断4号结点是否和1号结点相连,确认有,且cl+g4=29+8=37
不断搜索下去,到最后有以下组合和其值。
i | 1 | 2 | 3 | 4 | 5 | 权值 |
---|---|---|---|---|---|---|
结点组合 | 1 | 2 | 3 | 4 | 5 | 39 |
结点组合 | 1 | 2 | 3 | 5 | 4 | 37 |
结点组合 | 1 | 2 | 4 | 3 | 5 | 29 |
结点组合 | 1 | 2 | 5 | 3 | 4 | 23 |
结点组合 | 1 | 4 | 3 | 2 | 5 | 29 |
结点组合 | 1 | 4 | 3 | 5 | 2 | 23 |
结点组合 | 1 | 5 | 2 | 3 | 4 | 29 |
综上所述,bestl=23,路径为1-2-5-3-4-1。
代码实现
//初始化
void init()
{
bestl = INF;
cl = 0;
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
g[i][j] = g[j][i] = INF;
for (int i = 0; i <= n; i++)
{
x[i] = i;
bestx[i] = 0;
}
}
//邻接矩阵赋值
void createMatrix()
{
int u, v, w;//结点u和v,权值w
cout << "请依次输入两个结点u和v之间的权值w。" << endl;
cout << "格式:结点u 结点v 距离w" << endl;
for (int i = 1; i <= m; i++)
{
cin >> u >> v >> w;
g[u][v] = g[v][u] = w;
}
}
//回溯法核心
void backTrack(int t)
{
if (t > n)
//到达叶子结点
//最后一个结点与第一个结点有边相连且路径长度比当前最优值最小
//说明找到了一条更优路径,记录相关信息
{
if (g[n][1] != INF && (cl + g[x[n]][1] < bestl))
{
for (int j = 1; j <= n; j++)
bestx[j] = x[j];
bestl = cl + g[x[n]][1];
}
}
else
{
//没有到达叶子结点
for (int j = t; j <= n; j++)
{
//搜索扩展结点的所有分支
//如果第t-1个结点与第t个结点有边相连且可以得到更短路径
if (g[x[t - 1]][x[j]] != INF && (cl + g[x[t - 1]][x[j]] < bestl))
{
//保存第t个结点到x[t]中,进入第t+1个
swap(x[t], x[j]);//交换两个元素的值
cl = cl + g[x[t - 1]][x[t]];
backTrack(t + 1);
cl = cl - g[x[t - 1]][x[t]];
swap(x[t], x[j]);
}
}
}
}
//打印路径
void print()
{
cout << "最短路径:" << endl;
for (int i = 1; i <= n; i++)
cout << bestx[i] << "--->";
cout << "1" << endl;
cout << "最短路径长度:" << bestl << endl;
}
算法复杂度分析
- 时间复杂度:除了最后一层外,有1+(n-1)+...+(n-1)(n-2)...2<=n(n-1)!个结点需要判断约束函数和限界函数,判断两个函数需要O(1)的时间,因此耗时O(n!)。在叶子节点处记录当前最优解需要耗时O(n),在最坏情况下回搜索到每一个叶子结点,叶子数为(n-1)!,所以时间复杂度为O(n!)。
- 空间复杂度:O(n)。