先在“单次飞行不超过 D”的图上跑 Floyd 求任意两村庄间最短可达代价,再在这个距离矩阵上做状压 TSP。
OJ: luogu
题目 ID: P8733
难度:提高+/省选-
标签:状态压缩最短路Floyd动态规划
日期: 2026-06-21 05:17
题意
直升机从总部 1 号村庄出发,要访问所有村庄后再回到总部。
单次加满油后连续飞行距离不能超过 D,但每到一个村庄都可以重新加满油。
问最小总飞行距离。
思路
先看一个小数据验证版:
cpp
#include <bits/stdc++.h>
using namespace std;
const double INF = 1e100;
int n;
double limit_d;
double x[25], y_[25];
double dis_mat[25][25];
int used[25];
double ans;
double dist(int i, int j) {
double dx = x[i] - x[j];
double dy = y_[i] - y_[j];
return sqrt(dx * dx + dy * dy);
}
void dfs_perm(int last, int cnt, double cur) {
if (cur >= ans) {
return;
}
if (cnt == n) {
ans = min(ans, cur + dis_mat[last][1]);
return;
}
for (int i = 2; i <= n; i++) {
if (used[i] || dis_mat[last][i] >= INF / 2) {
continue;
}
used[i] = 1;
dfs_perm(i, cnt + 1, cur + dis_mat[last][i]);
used[i] = 0;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// brute.cpp:小数据先 Floyd,再直接枚举访问顺序。
cin >> n >> limit_d;
for (int i = 1; i <= n; i++) {
cin >> x[i] >> y_[i];
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
double d = dist(i, j);
if (d <= limit_d) {
dis_mat[i][j] = d;
} else {
dis_mat[i][j] = INF;
}
}
dis_mat[i][i] = 0;
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dis_mat[i][k] + dis_mat[k][j] < dis_mat[i][j]) {
dis_mat[i][j] = dis_mat[i][k] + dis_mat[k][j];
}
}
}
}
memset(used, 0, sizeof(used));
ans = INF;
dfs_perm(1, 1, 0.0);
cout.setf(ios::fixed);
cout << setprecision(2) << ans << '\n';
return 0;
}暴力的思路是:
- 先求出任意两点之间在补给限制下的最短可达代价
- 再直接枚举访问顺序
正解保留第 1 步,把第 2 步改成状压 DP。
关键点在于:
- 两村庄直线距离大于
D,并不代表永远不能到达 - 只要可以中途经过别的村庄补油,就仍然可能可达
所以必须先建图:
- 若两点欧氏距离
<= D,连一条边 - 否则视为不可直接飞行
然后跑 Floyd,得到 dis_mat[i][j]:
- 在油量限制下,从
i到j的最小可达代价
做到这一步之后,原题就被转化成标准的 TSP:
- 从
1出发 - 访问所有村庄
- 回到
1
于是再做状压 DP:
dp[mask][u]:已经访问了mask中这些村庄,当前停在u的最小代价
DP 转移方程
在 Floyd 处理出任意两点的补给最短路后,TSP 状压转移为:
最后枚举当前停点 u,再加上 dis_mat[u][1] 回到总部。
最后枚举回到总部即可。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
const double INF = 1e100;
int n;
double limit_d;
double x[MAXN], y[MAXN];
double dis_mat[MAXN][MAXN];
double dp[1 << 20][20];
double dist(int i, int j) {
double dx = x[i] - x[j];
double dy = y[i] - y[j];
return sqrt(dx * dx + dy * dy);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> limit_d;
for (int i = 1; i <= n; i++) {
cin >> x[i] >> y[i];
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
double d = dist(i, j);
if (d <= limit_d) {
dis_mat[i][j] = d;
} else {
dis_mat[i][j] = INF;
}
}
dis_mat[i][i] = 0.0;
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dis_mat[i][k] + dis_mat[k][j] < dis_mat[i][j]) {
dis_mat[i][j] = dis_mat[i][k] + dis_mat[k][j];
}
}
}
}
int full = 1 << (n - 1);
for (int mask = 0; mask < full; mask++) {
for (int i = 1; i <= n; i++) {
dp[mask][i] = INF;
}
}
dp[0][1] = 0.0;
for (int mask = 0; mask < full; mask++) {
for (int u = 1; u <= n; u++) {
if (dp[mask][u] >= INF / 2) {
continue;
}
for (int v = 2; v <= n; v++) {
if (mask & (1 << (v - 2))) {
continue;
}
if (dis_mat[u][v] >= INF / 2) {
continue;
}
int nmask = mask | (1 << (v - 2));
dp[nmask][v] = min(dp[nmask][v], dp[mask][u] + dis_mat[u][v]);
}
}
}
double ans = INF;
int all = full - 1;
for (int u = 1; u <= n; u++) {
if (dp[all][u] >= INF / 2 || dis_mat[u][1] >= INF / 2) {
continue;
}
ans = min(ans, dp[all][u] + dis_mat[u][1]);
}
cout.setf(ios::fixed);
cout << setprecision(2) << ans << '\n';
return 0;
}复杂度
时间复杂度
总结
这题最容易错的地方,是把它误看成“直接按欧氏距离做 TSP”。 真正的关键在于先把“中途补油”折叠进最短路,再做后续的状压 DP。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
