先 Floyd 求每个连通块内任意两点最短路,再枚举跨块连边,用两端点到各自块内最远点的距离更新合并后的最小直径。
OJ: luogu
题目 ID: P1522
难度:普及+/提高
标签:图论最短路Floyd连通块
日期: 2026-06-20 04:25
题意
给你 n 个牧区的坐标,以及一张 0/1 邻接矩阵。
1表示这两个牧区之间本来就有路- 路长等于这两个点的欧几里得距离
- 一个连通块就是一个牧场
现在必须在两个不同连通块之间新建一条路,要求新图中“各个连通块的直径最大值”尽量小,输出这个最小值。
这里的直径不是直线距离,而是:
- 同一个连通块里,两点之间最短路的最大值
思路
先看一个最直接的小数据暴力:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 155;
const long double INF = 1e100L;
int n;
long double x[MAXN], y[MAXN];
long double base_dist[MAXN][MAXN];
long double work_dist[MAXN][MAXN];
long double get_dist(int i, int j) {
long double dx = x[i] - x[j];
long double dy = y[i] - y[j];
return sqrtl(dx * dx + dy * dy);
}
void floyd(long double dist_arr[MAXN][MAXN]) {
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
if (dist_arr[i][k] >= INF / 2) {
continue;
}
for (int j = 1; j <= n; j++) {
if (dist_arr[k][j] >= INF / 2) {
continue;
}
long double nd = dist_arr[i][k] + dist_arr[k][j];
if (nd < dist_arr[i][j]) {
dist_arr[i][j] = nd;
}
}
}
}
}
long double calc_diameter(long double dist_arr[MAXN][MAXN]) {
long double ret = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dist_arr[i][j] >= INF / 2) {
continue;
}
if (dist_arr[i][j] > ret) {
ret = dist_arr[i][j];
}
}
}
return ret;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
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++) {
if (i == j) {
base_dist[i][j] = 0;
}
else {
base_dist[i][j] = INF;
}
}
}
for (int i = 1; i <= n; i++) {
string s;
cin >> s;
for (int j = 1; j <= n; j++) {
if (s[j - 1] == '1') {
base_dist[i][j] = get_dist(i, j);
}
}
}
floyd(base_dist);
long double answer = INF;
// 朴素思路:枚举每一条可加的新边,真的把它加上,再重跑一次 Floyd。
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
if (base_dist[i][j] < INF / 2) {
continue;
}
for (int a = 1; a <= n; a++) {
for (int b = 1; b <= n; b++) {
work_dist[a][b] = base_dist[a][b];
}
}
long double bridge = get_dist(i, j);
work_dist[i][j] = bridge;
work_dist[j][i] = bridge;
floyd(work_dist);
long double cand = calc_diameter(work_dist);
if (cand < answer) {
answer = cand;
}
}
}
cout << fixed << setprecision(6) << (double) answer << '\n';
return 0;
}这个暴力的做法很直接:
- 先求出原图的最短路
- 枚举一条原本不连通的点对
(i,j)作为新边 - 真的把这条边加进去
- 再跑一次 Floyd
- 统计加边后的最大有限最短路,也就是当前答案
这样写完全贴着题意走,但如果每条候选新边都重跑一次 Floyd,代价就太大了。
关键观察是:
如果把 i 和 j 连起来,那么新直径只可能来自三部分里的最大值:
- 原来某个连通块自己的直径
i所在连通块里的某个点走到i- 经过新边,再从
j走到它所在连通块里的某个点
也就是说,真正需要关心的是:
farthest[i]:点i到自己连通块内最远点的最短路长度
那么若新边连的是 (i,j),跨过这条新边形成的最长路长度就是:
farthest[i] + dist(i,j) + farthest[j]
下面这张图展示的就是这个结构:
graph LR A["块 1 中离 i 最远的点"] --- I["端点 i"] I -. "新边 dist(i,j)" .- J["端点 j"] J --- B["块 2 中离 j 最远的点"]
图里真正要看的不是具体边数,而是“新边左右两侧各接上一段块内最远路”。
所以只要先知道每个点在原连通块里的最远距离,就能立刻算出这条新边可能造成的最长跨块路径。
再和原有各个连通块的直径取最大值,就是连这条边后的总直径。
于是正式做法就是:
- 先 Floyd,求出原图任意两点最短路
- 对每个点
i,统计farthest[i] - 顺便求出原图所有连通块里的最大直径
old_diameter - 枚举所有原本不连通的点对
(i,j) - 计算候选值:
max(old_diameter, farthest[i] + dist(i,j) + farthest[j])
- 取最小值
和代码的对应关系也很清楚:
dist_arr:Floyd 后的最短路矩阵farthest_dist[i]:点i到本块最远点的距离old_diameter:原图所有连通块里的最大直径- 双重循环枚举原本不连通的
(i,j),更新答案
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 155;
const long double INF = 1e100L;
int n;
long double x[MAXN], y[MAXN];
long double dist_arr[MAXN][MAXN];
long double farthest_dist[MAXN];
long double get_dist(int i, int j) {
long double dx = x[i] - x[j];
long double dy = y[i] - y[j];
return sqrtl(dx * dx + dy * dy);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
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++) {
if (i == j) {
dist_arr[i][j] = 0;
}
else {
dist_arr[i][j] = INF;
}
}
}
for (int i = 1; i <= n; i++) {
string s;
cin >> s;
for (int j = 1; j <= n; j++) {
if (s[j - 1] == '1') {
dist_arr[i][j] = get_dist(i, j);
}
}
}
// 先求出原图中每个连通块内部的任意两点最短路。
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
if (dist_arr[i][k] >= INF / 2) {
continue;
}
for (int j = 1; j <= n; j++) {
if (dist_arr[k][j] >= INF / 2) {
continue;
}
long double nd = dist_arr[i][k] + dist_arr[k][j];
if (nd < dist_arr[i][j]) {
dist_arr[i][j] = nd;
}
}
}
}
long double old_diameter = 0;
for (int i = 1; i <= n; i++) {
farthest_dist[i] = 0;
for (int j = 1; j <= n; j++) {
if (dist_arr[i][j] >= INF / 2) {
continue;
}
if (dist_arr[i][j] > farthest_dist[i]) {
farthest_dist[i] = dist_arr[i][j];
}
}
if (farthest_dist[i] > old_diameter) {
old_diameter = farthest_dist[i];
}
}
long double answer = INF;
// 枚举新边的两个端点。只有原本不连通时,这条边才有意义。
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
if (dist_arr[i][j] < INF / 2) {
continue;
}
long double bridge = get_dist(i, j);
long double new_diameter = farthest_dist[i] + bridge + farthest_dist[j];
// 别的连通块可能还有更大的旧直径,也要一起保留。
long double cand = max(old_diameter, new_diameter);
if (cand < answer) {
answer = cand;
}
}
}
cout << fixed << setprecision(6) << (double) answer << '\n';
return 0;
}复杂度
Floyd 的时间复杂度是
后面统计 farthest 和枚举新边都是
所以总时间复杂度是:
空间复杂度是:
总结
这题最容易卡住的地方,是以为“每试一条新边都要重新跑最短路”。
其实 Floyd 只需要对原图跑一次。
之后每个点到本块最远点的距离已经固定了。
枚举新边 (i,j) 时,新增的影响只会体现在:
farthest[i] + dist(i,j) + farthest[j]
把这个式子想明白,这题就从“反复重算最短路”变成了“Floyd + 枚举连边”的标准模型。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
