非树点对只能补“路径最大边权+1”的边;按边权升序并查集合并,用块大小乘积批量累计所有点对的路径最大边权,O(n log n) 求最小完全图边权和。
OJ: roj
题目 ID: 1489
难度:普及+/提高-
标签:图论最小生成树Kruskal并查集贪心python
创建: 2026-09-30 14:15
更新: 2026-10-07 12:15
形式化题目
给定一棵
注意两点:
样例
graph LR
N1["点 1"] ---|"1"| N2["点 2"]
N1 ---|"1"| N3["点 3"]
N1 ---|"2"| N4["点 4"]
N2 -.->|"补 2"| N3
N3 -.->|"补 3"| N4
N2 -.->|"补 3"| N4
输出
正解
思路
朴素做法及其瓶颈。 最直接的思路:枚举每个非树点对
观察一:补边权的下界是
:把该边加入 形成环、删掉环上最大边,得到比 更小的生成树, 根本不是 MST; :同样换边得到等权的另一棵生成树,MST 不唯一,违反"有且仅有"; :按 Kruskal 的顺序,处理到这条边时它的路径上所有边(权都 )已把两端连通,它永远不会入树——所以 Kruskal 输出的就是 , 是 MST。
唯一性也由严格大于保证:任取另一棵生成树
边权是整数,所以每条补边的最小合法权值就是
其中相邻点对的
观察二:把点对按边聚合成块。 一条树边对应一大批点对——"分居该边两侧"的点对,其路径最大边权至少是这条边。按边权升序做 Kruskal 式合并:权
样例的合并与计数过程:
| 合并次序 | 边权 |
合并块大小 |
新跨块点对 | 贡献 |
|---|---|---|---|---|
| 1 | 1 | 1, 1 | 1 | |
| 2 | 1 | 2, 1 | 2 | |
| 3 | 2 | 3, 1 | 6 | |
| 合计 | 6 对(全部点对) | 9 |
表中三行覆盖了全部
代码
pair_max_edge_sum() 实现观察二的合并计数(循环体 total += w * size[ru] * size[rv] 就是 solve() 负责读入、按权排序,并用组合数闭式给每个非树点对补上
Python 版:
#!/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-09-30 14:18
# update_at: 2026-09-30 14:18
import sys
def find(root: list[int], x: int) -> int:
"""并查集查根:路径压缩后返回 x 所在连通块的代表点。"""
while root[x] != x:
root[x] = root[root[x]]
x = root[x]
return x
def pair_max_edge_sum(n: int, edges: list[tuple[int, int, int]]) -> int:
"""按边权升序合并连通块,求所有点对之间树上路径的最大边权之和。
合并时跨两块的点对路径必经过当前边,且块内边权都不超过它,
所以这批点对的"路径最大边权"恰为这条边的权值。
"""
root = list(range(n + 1)) # 父点表,下标 0 不用(点编号从 1 开始)
size = [1] * (n + 1)
total = 0
for u, v, w in edges:
ru, rv = find(root, u), find(root, v)
total += w * size[ru] * size[rv] # 跨块点对数 = 两块大小之积
root[ru] = rv
size[rv] += size[ru]
return total
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
edges = [(next(data), next(data), next(data)) for _ in range(n - 1)] # 依次读入 u、v、w
edges.sort(key=lambda e: e[2]) # 按边权升序,即 Kruskal 的合并顺序
# 全部点对的路径最大边权之和(树边已按原权计入),再给每个非树点对补 +1
ans = pair_max_edge_sum(n, edges) + n * (n - 1) // 2 - (n - 1)
print(ans)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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-05 04:05
* update_at: 2026-10-05 04:05
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
typedef long long ll;
struct Edge {
ll u;
ll v;
ll w;
};
ll n;
Edge edges[MAXN]; // 树的 n-1 条边,读入后按边权升序排序
ll root[MAXN]; // 并查集父点表,下标 0 不用(点编号从 1 开始)
ll size_of[MAXN]; // 块大小:合并时跨两块点对数 = 两块大小之积
// 并查集查根,带路径压缩。
ll find_root(ll x) {
while (root[x] != x) {
root[x] = root[root[x]];
x = root[x];
}
return x;
}
// 按边权升序合并连通块,累计所有点对树上路径最大边权之和。
// 合并权为 w 的边时,跨两块的 a*b 个点对路径必经过这条边,
// 且块内已用边权都不超过 w,所以这批点对的路径最大边权恰为 w。
ll pair_max_edge_sum() {
ll total = 0;
for (ll i = 1; i <= n - 1; i++) {
ll ru = find_root(edges[i].u);
ll rv = find_root(edges[i].v);
total += edges[i].w * size_of[ru] * size_of[rv];
root[ru] = rv;
size_of[rv] += size_of[ru];
}
return total;
}
bool cmp_edge(Edge a, Edge b) {
return a.w < b.w;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (ll i = 1; i <= n - 1; i++) {
cin >> edges[i].u >> edges[i].v >> edges[i].w;
}
for (ll i = 1; i <= n; i++) {
root[i] = i;
size_of[i] = 1;
}
sort(edges + 1, edges + n, cmp_edge); // 按边权升序,即 Kruskal 的合并顺序
// 每条非树点对的补边最小取"路径最大边权 + 1",非树点对共 C(n,2) - (n-1) 个
ll ans = pair_max_edge_sum() + n * (n - 1) / 2 - (n - 1);
cout << ans << "\n";
return 0;
}复杂度
- 时间:排序
,并查集(路径压缩) ,总计 ,与同算法的 C++ 正解同阶。 - 空间:
,存边、父点表和块大小各一份。 - 答案最大约
,Python int自动支持大整数;本地实测最大点 0.099s、峰值 55.6MB,对 1000ms / 128MB 的限制留有余量。
总结
- 唯一 MST 的充要条件:每条非树边权严格大于它两端点在
上的路径最大边权;整数权下补边最小取 。 - 目标拆成两块:
(含树边和)+ 非树点对数,后者是组合数闭式。 不逐点对求,而是按边权升序并查集合并,用"块大小乘积 边权"批量累计——把 的点对枚举压成 次合并,整体 。