按 Kruskal 合并事件给非树边批量定价:合并大小为 a、b 的两块时产生 a·b−1 条非树边,每条最小合法权值为 w+1,全部累加即为答案。
OJ: roj
题目 ID: 3185
难度:提高
标签:图论最小生成树Kruskal并查集
创建: 2026-10-01 23:58
更新: 2026-10-10 13:00
形式化题目
给定一棵
正解
思路
先算一条非树边能取多小
目标是「新增边权之和」,而「
设
- 填
:把路径上权值为 的树边记为 。在 中加入 、 删去 得到另一棵生成树,权值为 ,与 是最小生成树矛盾。 - 填
:同样的替换得到一棵权值相等的生成树 ,与 是唯一 MST 矛盾。 - 填
:任取 与 。 上加 形成唯一圈,圈上必有一条树边 不在 中(否则 里 已连通, 会成圈), 于是 ,用 换掉 使 权值严格减小。所以任何 都不是最小生成树, 唯一。
也就是:
卡点: 条非树边的枚举跑不完
按这个结论,只要对每条非树边求一次「路径最大边权」即可。但非树边有
关键观察:路径最大边权 = 首次连通时的树边权值
把 Kruskal 的过程看成在按权值递增接受树边。当
于是不要枚举非树边,改去枚举合并事件:合并事件只有
批量定价
设当前处理权值为
- 有
个点对,其中 、 ; - 其中恰好一个是刚被接受的树边
本身; - 剩下
个都是非树边,且它们的 恰好等于 。
所以每条填
| 样例 | 树边 |
A 块大小 | B 块大小 | 新增非树边数 | 每条权值 | 本步贡献 | 累计 | |
|---|---|---|---|---|---|---|---|---|
| 一 | 2 | 1 | 1 | 0 | 3 | 0 | 0 | |
| 一 | 3 | 2 | 1 | 1 | 4 | 4 | 4 | |
| 二 | 3 | 1 | 1 | 0 | 4 | 0 | 0 | |
| 二 | 4 | 2 | 1 | 1 | 5 | 5 | 5 | |
| 二 | 5 | 3 | 1 | 2 | 6 | 12 | 17 |
每行是一次 Kruskal 合并:先给出这次连接的两块大小,再给出这批非树边的条数与单价。
第一组样例只有第 2 步有贡献——补上唯一的非树边
第一组样例补完后的完全图如下:原树边是
flowchart LR
n1(("1"))
n2(("2"))
n3(("3"))
n1 ---|"2 原树边"| n2
n1 ---|"3 原树边"| n3
n2 ---|"4 新增边"| n3
观察重点是
实现要点
- 并查集除
parent外维护每个根的size,合并时小树挂大树,保证近。 - 边存成
(weight, x, y)元组,sorted直接给出 Kruskal 顺序,不需要额外的key=。 - 累加必须在合并之前完成,否则读到的块大小已经被改掉。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 13:00
* update_at: 2026-10-10 13:00
*/
// 秘密的牛奶运输:按 Kruskal 顺序累加“补全完全图”所需的最小新增边权
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXN = 500005;
int parent[MAXN];
ll sz[MAXN]; // 只有根节点的 size 有意义
int find_root(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
struct Edge { // 树边:权值放首位便于排序
ll w;
int x;
int y;
};
bool cmp(const Edge& a, const Edge& b) {
if (a.w != b.w) {
return a.w < b.w;
}
if (a.x != b.x) {
return a.x < b.x; // 与 Python 元组排序的并列处理保持一致
}
return a.y < b.y;
}
int main() {
int cases;
if (scanf("%d", &cases) != 1) {
return 0;
}
for (int tc = 0; tc < cases; tc++) {
int n;
scanf("%d", &n);
vector<Edge> edges(n > 1 ? n - 1 : 0);
for (int i = 0; i + 1 < n; i++) {
int x, y;
ll w;
scanf("%d %d %lld", &x, &y, &w);
edges[i].w = w;
edges[i].x = x;
edges[i].y = y;
}
sort(edges.begin(), edges.end(), cmp);
for (int i = 1; i <= n; i++) {
parent[i] = i;
sz[i] = 1;
}
ll total = 0;
for (int i = 0; i < (int)edges.size(); i++) {
int rx = find_root(edges[i].x), ry = find_root(edges[i].y);
// 这两块之间除这条树边外的点对,每条最小可行权值都是 w+1
total += (sz[rx] * sz[ry] - 1) * (edges[i].w + 1);
if (sz[rx] < sz[ry]) { // 按大小合并
int t = rx;
rx = ry;
ry = t;
}
parent[ry] = rx;
sz[rx] += sz[ry];
}
printf("%lld\n", total);
}
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 23:58
# update_at: 2026-10-02 00:03
import sys
def find(parent: list[int], x: int) -> int:
"""路径减半地找 x 所在并查集的根,顺带把沿途节点挂向祖父。"""
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def minimum_added_weight(n: int, tree_edges: list[tuple[int, int, int]]) -> int:
"""补成完全图且原树仍是唯一 MST 时,新加边权和的最小值。
按 Kruskal 的顺序(权值递增)接受树边:轮到权值 w 的边 (x,y) 时,两端点所在的
连通块 A、B 内部的边已经被全部定好,此刻两个块之间唯一确定还存在的是这条树边。
要让原树保持唯一 MST,A×B 里其余 |A|·|B|-1 条边的最小可行权值就是 w+1:
取 w 会让它们与该树边并列(MST 不唯一),取更小则原树不再是 MST。
"""
parent = list(range(n + 1))
size = [1] * (n + 1) # 只有根节点的 size 有意义
total = 0
for weight, x, y in sorted(tree_edges):
root_x, root_y = find(parent, x), find(parent, y)
# 这两块之间除这条树边外的点对,路径最大树边权都是 weight,所以每条填 weight+1
added = (size[root_x] * size[root_y] - 1) * (weight + 1)
total += added
if size[root_x] < size[root_y]: # 按大小合并,控制树的深度
root_x, root_y = root_y, root_x
parent[root_y] = root_x
size[root_x] += size[root_y]
return total
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
test_count = next(data)
for _ in range(test_count):
n = next(data)
tree_edges: list[tuple[int, int, int]] = []
for _ in range(n - 1):
x, y, weight = next(data), next(data), next(data)
tree_edges.append((weight, x, y)) # 权值放首位,交给 sorted 排 Kruskal 序
out.append(str(minimum_added_weight(n, tree_edges)))
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
每组,其中排序 、并查集 。 相比枚举全部非树边的 做法有本质提升; 时实测约 0.03s。 - 空间复杂度:
,即并查集两个数组与边列表。
总结
- 目标函数可拆时,先求「每条非树边单独的最小合法权值」,再把各边的局部下界相加。
是唯一 MST 的充要条件:每条非树边严格大于其树上路径的最大边权。 - 把「路径最大边权」换成 Kruskal 的视角,它就成了「两点首次连通时那条边的权值」;
于是
条非树边的枚举被压成 次合并事件,每次 批量定价。 - 并查集按大小合并,既控制树高,又让批量定价所需的「两块大小」随手可得。