[USACO2.4] 牛的旅行 Cow Tours

GitHub跳转原题关系图返回列表

先 Floyd 求每个连通块内任意两点最短路,再枚举跨块连边,用两端点到各自块内最远点的距离更新合并后的最小直径。

OJ: luogu

题目 ID: P1522

难度:普及+/提高

标签:图论最短路Floyd连通块

日期: 2026-06-20 04:25

题意

给你 n 个牧区的坐标,以及一张 0/1 邻接矩阵。

  • 1 表示这两个牧区之间本来就有路
  • 路长等于这两个点的欧几里得距离
  • 一个连通块就是一个牧场

现在必须在两个不同连通块之间新建一条路,要求新图中“各个连通块的直径最大值”尽量小,输出这个最小值。

这里的直径不是直线距离,而是:

  • 同一个连通块里,两点之间最短路的最大值

思路

先看一个最直接的小数据暴力:

cpp
#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;
}

这个暴力的做法很直接:

  1. 先求出原图的最短路
  2. 枚举一条原本不连通的点对 (i,j) 作为新边
  3. 真的把这条边加进去
  4. 再跑一次 Floyd
  5. 统计加边后的最大有限最短路,也就是当前答案

这样写完全贴着题意走,但如果每条候选新边都重跑一次 Floyd,代价就太大了。

关键观察是:
如果把 ij 连起来,那么新直径只可能来自三部分里的最大值:

  1. 原来某个连通块自己的直径
  2. i 所在连通块里的某个点走到 i
  3. 经过新边,再从 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 最远的点"]

图里真正要看的不是具体边数,而是“新边左右两侧各接上一段块内最远路”。
所以只要先知道每个点在原连通块里的最远距离,就能立刻算出这条新边可能造成的最长跨块路径。
再和原有各个连通块的直径取最大值,就是连这条边后的总直径。

于是正式做法就是:

  1. 先 Floyd,求出原图任意两点最短路
  2. 对每个点 i,统计 farthest[i]
  3. 顺便求出原图所有连通块里的最大直径 old_diameter
  4. 枚举所有原本不连通的点对 (i,j)
  5. 计算候选值:
    • max(old_diameter, farthest[i] + dist(i,j) + farthest[j])
  6. 取最小值

和代码的对应关系也很清楚:

  • dist_arr:Floyd 后的最短路矩阵
  • farthest_dist[i]:点 i 到本块最远点的距离
  • old_diameter:原图所有连通块里的最大直径
  • 双重循环枚举原本不连通的 (i,j),更新答案

代码

cpp
#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 的时间复杂度是 O(n3)O(n^3)

后面统计 farthest 和枚举新边都是 O(n2)O(n^2)

所以总时间复杂度是:

  • O(n3)O(n^3)

空间复杂度是:

  • O(n2)O(n^2)

总结

这题最容易卡住的地方,是以为“每试一条新边都要重新跑最短路”。

其实 Floyd 只需要对原图跑一次。
之后每个点到本块最远点的距离已经固定了。
枚举新边 (i,j) 时,新增的影响只会体现在:

  • farthest[i] + dist(i,j) + farthest[j]

把这个式子想明白,这题就从“反复重算最短路”变成了“Floyd + 枚举连边”的标准模型。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析