[USACO07DEC] Building Roads S

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

把已有道路先用并查集合并,再在所有点对构成的完全图上跑 Kruskal 求最小新增长度。

OJ: luogu

题目 ID: P2872

难度:普及/提高-

标签:最小生成树Kruskal并查集几何

日期: 2026-01-03 09:56

题意

给定 n 个点的坐标,以及 m 条已经建好的道路。

现在可以在任意两个点之间新建道路,费用为两点之间的欧氏距离。要求新增一些道路,使所有点连通,并让新增道路总长度最小。

思路

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

// brute.cpp:Prim 版本最小生成树,用不同实现辅助对拍。

const int MAXN = 105;
const double INF = 1e100;

int n, m;
long long x_pos[MAXN], y_pos[MAXN];
double cost[MAXN][MAXN];
double dist_to_tree[MAXN];
bool used[MAXN];

double distance_between(int i, int j) {
    double dx = (double)(x_pos[i] - x_pos[j]);
    double dy = (double)(y_pos[i] - y_pos[j]);
    return sqrt(dx * dx + dy * dy);
}

void read_input() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> x_pos[i] >> y_pos[i];
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            cost[i][j] = distance_between(i, j);
        }
    }

    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        cost[u][v] = 0.0;
        cost[v][u] = 0.0;
    }
}

double prim() {
    for (int i = 1; i <= n; i++) {
        dist_to_tree[i] = INF;
        used[i] = false;
    }
    dist_to_tree[1] = 0.0;

    double answer = 0.0;
    for (int step = 1; step <= n; step++) {
        int u = 0;
        for (int i = 1; i <= n; i++) {
            if (!used[i] && (u == 0 || dist_to_tree[i] < dist_to_tree[u])) {
                u = i;
            }
        }

        used[u] = true;
        answer += dist_to_tree[u];

        for (int v = 1; v <= n; v++) {
            if (!used[v] && cost[u][v] < dist_to_tree[v]) {
                dist_to_tree[v] = cost[u][v];
            }
        }
    }

    return answer;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    read_input();
    cout << fixed << setprecision(2) << prim() << '\n';

    return 0;
}

这道题是最小生成树模型。区别在于有些道路已经存在,不需要再花钱。

可以把已有道路看作费用为 0 的边。实现时不必把它们加入边数组排序,直接在一开始用并查集合并它们的两个端点即可。

然后枚举所有点对,计算欧氏距离,得到一张完全图。因为 n <= 1000,完全图大约有 5 * 10^5 条边,可以接受。

最终做 Kruskal:

  1. 已有道路先合并;
  2. 所有候选新道路按长度排序;
  3. 如果当前边连接了两个不同连通块,就选它并累加长度;
  4. 全部连通后得到最小新增长度。

brute.cpp 用 Prim 在完整费用矩阵上求 MST,和正解的 Kruskal 实现不同,用于小数据验证。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1005;
const int MAXE = 1000005;

struct Edge {
    int u, v;
    double w;
};

int n, m;
long long x_pos[MAXN], y_pos[MAXN];
Edge edge[MAXE];
int edge_count;
int father[MAXN];

bool cmp_edge(const Edge &a, const Edge &b) {
    return a.w < b.w;
}

int find_root(int x) {
    if (father[x] == x) {
        return x;
    }
    father[x] = find_root(father[x]);
    return father[x];
}

bool merge_set(int x, int y) {
    int rx = find_root(x);
    int ry = find_root(y);
    if (rx == ry) {
        return false;
    }
    father[rx] = ry;
    return true;
}

double distance_between(int i, int j) {
    double dx = (double)(x_pos[i] - x_pos[j]);
    double dy = (double)(y_pos[i] - y_pos[j]);
    return sqrt(dx * dx + dy * dy);
}

void add_edge(int u, int v, double w) {
    edge_count++;
    edge[edge_count].u = u;
    edge[edge_count].v = v;
    edge[edge_count].w = w;
}

void read_input() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> x_pos[i] >> y_pos[i];
        father[i] = i;
    }

    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        merge_set(u, v); // 已经存在的路不需要再付费。
    }
}

void build_all_edges() {
    edge_count = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            add_edge(i, j, distance_between(i, j));
        }
    }
}

void solve() {
    build_all_edges();
    sort(edge + 1, edge + edge_count + 1, cmp_edge);

    double answer = 0.0;
    for (int i = 1; i <= edge_count; i++) {
        if (merge_set(edge[i].u, edge[i].v)) {
            answer += edge[i].w;
        }
    }

    cout << fixed << setprecision(2) << answer << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    read_input();
    solve();

    return 0;
}

复杂度

完全图边数为 O(n2)O(n^2),排序复杂度为 O(n2logn)O(n^2 log n)

空间复杂度为 O(n2)O(n^2)

总结

“已有边 + 最小新增代价连通”可以直接转成 MST。

已有道路先合并,相当于已经免费选入;剩下的工作就是在所有可能新道路中按 Kruskal 选择最短的连接边。