每周只新增一条边,因此当前最小生成树只可能通过“接上一条新边”或“用新边替换环上的更大边”发生变化;维护最小生成森林即可在线输出答案。
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 可能会被改写。
思路
先看一个最直接的小数据暴力:
// 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)。
那么新的最优结构只会有两种情况:
-
u和v原来不连通
这条边会把两个连通块接起来,直接加入当前森林。 -
u和v原来已经连通
把这条边加进去会在当前树里形成一个环。 如果环上存在比它更重的边,就应该把那条更重的边换掉;否则这条新边没用。
这就是最小生成树在“单条加边”场景下的经典更新方式。
由于本题 N <= 200 很小,不需要上复杂动态树。
我们可以直接维护当前森林边集,并在需要时:
- 用 BFS 找
u到v的路径 - 扫出这条路径上权值最大的边
- 决定是否替换
这样每周的修改就是
代码
#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 条边,所以单次更新复杂度是
总复杂度可以看成:
空间复杂度
总结
这题的关键不是“每周都重跑 MST”,而是抓住“每次只多一条边”这个增量特征。只要想到 MST 在加一条边后只会发生一次局部替换,整题就能在线维护过去。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
