双调欧几里得旅行商问题(双调TSP)CLRS算法导论 思考题15-3 讲解(动态规划证明最优子结构)以及代码
双调欧几里得旅行商问题
题目
在欧几里得旅行商问题中,给定平面$n$个点作为输入,希望求出连接所有$n$个点的最短巡游路线。此问题是NP难问题,因此不存在多项式时间的求解算法。
J.L.Bentley建议将问题简化,限制巡游路线为双调巡游(bitonic tours):从最左边的点开始,严格向右前进,直至最右边的点,然后调头严格向左前进,直至回到起始点。问题简化后存在多项式时间算法。
要求:设计一个$\mathcal{O}(n^2)$时间的最优双调巡游路线算法。假设任何两个点的$x$坐标均不同,且所有实数运算都花费单位时间。
最优解的结构特征
标准TSP的状态压缩解法无法达到$\mathcal{O}(n^2)$复杂度,而双调巡游具有x坐标单调的约束,我们可以利用该特性拆分子问题:
- 预处理:将所有点按$x$坐标从小到大排序,记为:
$$p_1, p_2, \ldots, p_n \ (x_1 < x_2 < \ldots < x_n)$$ - 核心思路:合法双调巡游可拆分为两条从$p_1$出发的单调向右路径,最终在$p_n$处汇合形成回路,两条路径覆盖所有点且不重复。
子问题定义
定义$B(i,j)$:覆盖点$p_1,p_2,\dots,p_i$的两条不相交路径,满足:
- 所有点仅出现一次;
- 两条路径均从$p_1$开始;
- 路径末端为$p_i$和$p_j$($1 \leq j < i$);
- 每条路径严格向右(x坐标递增)。
最优子结构
-
若 $i > j + 1$
最优解$B(i,j)$中必有边$(p_{i-1}, p_i)$,去掉该边后,剩余结构为子问题$B(i-1,j)$的最优解。 -
若 $i = j + 1$
存在$k < j$,最优解$B(i,j)$中必有边$(p_k, p_i)$,去掉该边后,剩余结构为子问题$B(i-1,k)$的最优解。
最优子结构证明
情况1:$i > j+1$
- $B(i,j)$的末端为$p_i$、$p_j$,$j < i-1$,因此$p_{i-1}$不是路径末端;
- 路径必须严格向右,$p_{i-1}$只能连接到唯一更右侧的点$p_i$;
- 若去掉边$(p_{i-1},p_i)$后剩余结构不是$B(i-1,j)$的最优解,则可构造更优的$B(i,j)$,与最优性矛盾。
情况2:$i = j+1$
- 两条路径末端为$p_i$和$p_{i-1}$,若连接$p_{i-1}$和$p_i$会破坏双路径结构;
- 因此$p_i$必须连接到另一条路径的末端$p_k$($k<j$);
- 同理可证,去掉边$(p_k,p_i)$后剩余结构为$B(i-1,k)$的最优解。
递归求解方案
定义$OPT(i,j)$为$B(i,j)$的最短路径长度,$d(a,b)$表示点$a$和点$b$的欧几里得距离。
状态转移方程
- 初始条件:$OPT(2,1) = d(p_1,p_2)$
- 当 $i > j + 1$:
$$OPT(i,j) = OPT(i-1,j) + d(p_{i-1}, p_i)$$ - 当 $i = j + 1$:
$$OPT(i,j)=\min_{k < j} {OPT(i-1,k) + d(p_k, p_i)}$$
动态规划算法实现
算法:BITONIC-TSP
BITONIC-TSP(P)
按x坐标对P排序
n = 点的数量
初始化 OPT[1..n][1..n] 二维表
初始化 PREV[1..n][1..n] 前驱记录表
OPT[2][1] = 点p1到p2的距离
PREV[2][1] = (1, 1)
for i = 3 to n:
// 情况1:i > j+1
for j = 1 to i-2:
OPT[i][j] = OPT[i-1][j] + dist(P[i-1], P[i])
PREV[i][j] = (i-1, j)
// 情况2:i = j+1
OPT[i][i-1] = 无穷大
for k = 1 to i-2:
val = OPT[i-1][k] + dist(P[k], P[i])
if val < OPT[i][i-1]:
OPT[i][i-1] = val
PREV[i][i-1] = (i-1, k)
return OPT, PREV
变量说明
- $OPT[i][j]$:覆盖前$i$个点,路径端点为$p_i$和$p_j$($i>j$)的最短双调路径长度;
- $PREV[i][j]$:记录状态$(i,j)$的前驱状态,用于还原路径。
复杂度分析
- 排序:$\mathcal{O}(n\log n)$
- 状态总数:$\frac{n(n-1)}{2}=\mathcal{O}(n^2)$
- 总时间复杂度:$\boldsymbol{\mathcal{O}(n^2)}$
- 空间复杂度:$\boldsymbol{\mathcal{O}(n^2)}$
最终闭合回路
最短双调巡游长度 = $OPT[n][n-1] + d(p_{n-1}, p_n)$
最优路径构造
算法:RECONSTRUCT
RECONSTRUCT(PREV, P, n)
i = n
j = n - 1
left = []
right = []
while i > 1:
prev_i, prev_j = PREV[i][j]
if prev_j == j:
left.append(P[i])
i = i - 1
else:
right.append(P[i])
i = i - 1
j = prev_j
left.append(P[1])
反转left
tour = left + right
return tour
复杂度
- 时间复杂度:$\mathcal{O}(n)$(仅需回溯$n$次)
总结
- 核心思想:利用双调巡游的单调性,将问题拆分为两条向右的路径,用动态规划求解;
- 状态设计:$OPT[i][j]$记录两条路径的端点,保证无后效性;
- 时间复杂度:$\mathcal{O}(n^2)$,完美满足题目要求;
- 完整流程:排序→动态规划填表→回溯构造最优路径。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const double INF = 1e18;
class Point {
public:
double x, y;
};
// 欧几里得距离
double dist(Point a, Point b) {
return hypot(a.x - b.x, a.y - b.y);
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int n;
while (cin >> n) {
vector<Point> P(n + 1); // 1-based
for (int i = 1; i <= n; i++) {
cin >> P[i].x >> P[i].y;
}
vector<vector<double>> OPT(n + 1, vector<double>(n + 1, INF));
OPT[2][1] = dist(P[1], P[2]);
for (int i = 3; i <= n; i++) {
for (int j = 1; j <= i - 2; j++) {
OPT[i][j] = OPT[i - 1][j] + dist(P[i - 1], P[i]);
}
OPT[i][i - 1] = INF;
for (int k = 1; k <= i - 2; k++) {
double val = OPT[i - 1][k] + dist(P[k], P[i]);
OPT[i][i - 1] = min(OPT[i][i - 1], val);
}
}
double ans = OPT[n][n - 1] + dist(P[n - 1], P[n]);
cout << fixed << setprecision(2) << ans << "\n";
}
return 0;
}

浙公网安备 33010602011771号