[IOI 2003] 兽径管理

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

每周只新增一条边,因此当前最小生成树只可能通过“接上一条新边”或“用新边替换环上的更大边”发生变化;维护最小生成森林即可在线输出答案。

OJ: luogu

题目 ID: P1340

难度:提高+/省选-

标签:图论最小生成树思维

日期: 2026-06-20 01:18

题意

一共有 N 个草地,每周会新发现一条兽径。

在第 i 周开始时,你可以从前 i 条已知兽径里选一些来管理,使得所有草地连通,并让总长度最小。

如果当前还无法让整张图连通,就输出 -1

也就是说:对每个前缀 1..i,都要求一次最小生成树的边权和。

样例过程表

这张表把样例里每周的答案列出来:

周数 新增兽径 当前最优答案
1 (1,2,10) -1
2 (1,3,8) -1
3 (3,2,3) -1
4 (1,4,3) 14
5 (1,3,6) 12
6 (2,1,2) 8

可以看到,每次只是多了一条新边,但当前 MST 可能会被改写。

思路

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

cpp
// brute.cpp:每一周都把前缀边重新跑一遍 Kruskal。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 20;
const int MAXM = 105;

struct Edge {
    int u, v, w;

    bool operator<(const Edge &other) const {
        return w < other.w;
    }
} edges[MAXM];

int n, weeks;
int fa[MAXN];

void init_dsu() {
    for (int i = 1; i <= n; i++) {
        fa[i] = i;
    }
}

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

bool unite(int x, int y) {
    x = find_root(x);
    y = find_root(y);
    if (x == y) {
        return false;
    }
    fa[x] = y;
    return true;
}

long long solve_prefix(int cnt) {
    vector<Edge> a;
    for (int i = 1; i <= cnt; i++) {
        a.push_back(edges[i]);
    }
    sort(a.begin(), a.end());

    init_dsu();
    int used = 0;
    long long sum = 0;

    for (int i = 0; i < (int)a.size(); i++) {
        if (!unite(a[i].u, a[i].v)) {
            continue;
        }
        used++;
        sum += a[i].w;
        if (used == n - 1) {
            break;
        }
    }

    if (used != n - 1) {
        return -1;
    }
    return sum;
}

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

    cin >> n >> weeks;
    for (int i = 1; i <= weeks; i++) {
        cin >> edges[i].u >> edges[i].v >> edges[i].w;
    }

    for (int i = 1; i <= weeks; i++) {
        cout << solve_prefix(i) << '\n';
    }

    return 0;
}

暴力对每一周都把前缀边重新拿出来,完整跑一遍 Kruskal。

这个方法很直观,但如果每周都从头来,会有不少重复工作。

关键观察是:每周只新增一条边。

设上一周已经维护好了当前前缀图的最小生成森林,新增边为 (u,v,w)。 那么新的最优结构只会有两种情况:

  1. uv 原来不连通
    这条边会把两个连通块接起来,直接加入当前森林。

  2. uv 原来已经连通
    把这条边加进去会在当前树里形成一个环。 如果环上存在比它更重的边,就应该把那条更重的边换掉;否则这条新边没用。

这就是最小生成树在“单条加边”场景下的经典更新方式。

由于本题 N <= 200 很小,不需要上复杂动态树。 我们可以直接维护当前森林边集,并在需要时:

  • 用 BFS 找 uv 的路径
  • 扫出这条路径上权值最大的边
  • 决定是否替换

这样每周的修改就是 O(N)O(N) 级别。

代码

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

const int MAXN = 205;

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

int n, weeks;
vector<Edge> forest;
long long total_weight = 0;

vector<pair<int, int>> graph[MAXN];
bool vis[MAXN];
int parent_node[MAXN];
int parent_edge[MAXN];

void build_graph() {
    for (int i = 1; i <= n; i++) {
        graph[i].clear();
    }
    for (int i = 0; i < (int)forest.size(); i++) {
        Edge &e = forest[i];
        graph[e.u].push_back({e.v, i});
        graph[e.v].push_back({e.u, i});
    }
}

// 如果 u 和 v 当前在同一棵树里,返回路径上权值最大的边编号。
// 否则返回 -1,表示这条新边会连接两个不同连通块。
int max_edge_on_path(int start, int target) {
    build_graph();
    for (int i = 1; i <= n; i++) {
        vis[i] = false;
        parent_node[i] = 0;
        parent_edge[i] = -1;
    }

    queue<int> q;
    q.push(start);
    vis[start] = true;

    while (!q.empty()) {
        int u = q.front();
        q.pop();

        if (u == target) {
            break;
        }

        for (pair<int, int> e : graph[u]) {
            int v = e.first;
            int id = e.second;
            if (vis[v]) {
                continue;
            }
            vis[v] = true;
            parent_node[v] = u;
            parent_edge[v] = id;
            q.push(v);
        }
    }

    if (!vis[target]) {
        return -1;
    }

    int best_id = -1;
    int cur = target;
    while (cur != start) {
        int id = parent_edge[cur];
        if (best_id == -1 || forest[id].w > forest[best_id].w) {
            best_id = id;
        }
        cur = parent_node[cur];
    }

    return best_id;
}

void add_new_edge(Edge e) {
    int id = max_edge_on_path(e.u, e.v);

    if (id == -1) {
        forest.push_back(e);
        total_weight += e.w;
        return;
    }

    if (forest[id].w > e.w) {
        total_weight += e.w - forest[id].w;
        forest[id] = e;
    }
}

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

    cin >> n >> weeks;

    for (int i = 1; i <= weeks; i++) {
        Edge e;
        cin >> e.u >> e.v >> e.w;
        add_new_edge(e);

        if ((int)forest.size() == n - 1) {
            cout << total_weight << '\n';
        } else {
            cout << -1 << '\n';
        }
    }

    return 0;
}

复杂度

设草地数为 N,周数为 W

每次新加一条边时:

  • 先用当前森林建一张小图
  • 再做一次 BFS 找路径

因为森林里最多只有 N-1 条边,所以单次更新复杂度是 O(N)O(N)

总复杂度可以看成:

O(WN)O(WN)

空间复杂度 O(N)O(N)

总结

这题的关键不是“每周都重跑 MST”,而是抓住“每次只多一条边”这个增量特征。只要想到 MST 在加一条边后只会发生一次局部替换,整题就能在线维护过去。

一图流解析

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

一图流解析