给定一张 n 个点、m 条带权边的连通图,从中选 n−1 条边把所有点连起来,使边权和最小——这就是最小生成树(MST)。先记住一个事实:n 个点、n−1 条边、连通,这三条同时成立时,图一定是树——任意两点之间路径唯一,没有环。于是“选一棵树”只剩一个自由度:到底选哪 n−1 条边。
Kruskal 的正文只有三句话:把边按权值升序排;从小到大逐条看;两端还没连通就要它,已经连通就扔掉。“连通”这个词在图上很难判,但对一张不断合并的图来说,它恰好是并查集的全部工作。
| Kruskal | Prim(堆优化) | |
|---|---|---|
| 套路 | 边按权值升序,能加就加,成环就跳 | 从一点出发,每次把离生成树最近的点拉进来 |
| 依赖 | 边排序 + 并查集判环 | 邻接表 + 优先队列取最近点 |
| 复杂度 | O(m log m) | O((n + m) log n) |
| 稀疏图 m ≈ n | 快,只跟边数有关 | 差不多,但堆操作多 |
| 稠密图 m ≈ n² | 要排 n² 条边,偏慢 | 更快;n 小时用 O(n²) 的邻接矩阵版 |
| 附带产物 | 边权从小到大的合并过程,能直接回答瓶颈路 | 每个点被拉进来时的距离,是它到已选点集的最小边 |
| 本节为什么用 Kruskal | ★ 判环用并查集,只有 5 行,且不用建邻接表 | 第 42 节已学,思路是 Dijkstra 的孪生兄弟 |
先拆三块骨头:find、merge、Kruskal 主循环。前两个是并查集(判“在不在一块”),第三个是贪心框架。这三块加起来不到 20 行,但每一行都有一个专属的错法。
| 组成 | 含义 / 格式要求 |
|---|---|
fa[x] | x 的父指针。fa[x] == x 时 x 就是所在集合的代表(根)。初始化 fa[i] = i,每个点自成一个集合。 |
fa[x] == x ? x : | 递归出口:自己就是根,返回自己。整个函数只有这一个终止条件。 |
fa[x] = find(fa[x]) | 先递归找到根,再把 x 直接挂到根下面——这就是路径压缩。赋值号不能省,省掉就只剩递归、没有压缩。 |
路径压缩的效果 | 链 a→b→c→root:第一次 find(a) 沿链走到底,返回时顺手把 a、b、c 全改成指向 root;之后再问就是 O(1)。 |
返回值 | 根节点的编号。它只用来判“是不是同一集合”,不代表任何顺序或大小。 |
为什么递归不会爆栈 | 没压缩前树高最多 log₂n(按大小合并保证),n = 5000 时不过 13 层;压缩后更浅。 |
初始化必写 | for (int i = 1; i <= n; ++i) fa[i] = i; 漏写时 fa 全是 0,所有点被认成同一集合 → 一条边都加不进去 → WA。 |
三目写法 | 比 if/else 短,能直接写成 return 表达式,编译器容易内联。竞赛里几乎只写这一种。 |
#include <bits/stdc++.h>
using namespace std;
int fa[10];
int find(int x) { // 找根 + 顺手压缩
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
int main() {
for (int i = 1; i <= 5; ++i) fa[i] = i; // 初始化:各自是根
fa[3] = 2; fa[2] = 1; // 手工造一条链 3 → 2 → 1
cout << find(3) << "\n"; // 1:根是 1
cout << fa[3] << "\n"; // 1:压缩后 3 直接指向 1
cout << fa[2] << "\n"; // 1:2 也被压平了
return 0;
}压缩发生在返回的路上:先递归到根,再一层层把 fa[x] 改成根。所以第一次调用最慢,之后全是 O(1)。
fa 数组是全局的,多组数据时必须重新 for (i) fa[i] = i,不然上一组的关系会被继承下来。
| 组成 | 含义 / 格式要求 |
|---|---|
x = find(x); y = find(y); | 先把两个点换成各自的根。传进来的是普通点,直接 fa[y] = x 会把一条路径接乱,之后全错。 |
if (x == y) return; | 已在同一集合,合并是空操作。漏写不崩,但 size 会重复累加、判环失效(见易错清单)。 |
if (sz[x] < sz[y]) swap(x, y); | 保证 x 是大的那块。把小树挂到大树底下,树高最多涨 1。这就是按大小合并。 |
fa[y] = x; | 把 y 的根接到 x 的根下面。整个集合的结构只靠这一行改动。 |
sz[x] += sz[y]; | 只维护新根 x 的 size。y 已经不是根了,它的 size 再没人读,不用清。 |
返回值 | void。想知道“这次合并是否真的发生”,判 find(u) == find(v) 更好——Kruskal 正是靠它跳过成环的边。 |
按秩还是按大小 | 维护 size 比维护 rank(树高)简单,效果一样:都给 O(log n) 上界,配上路径压缩就都是 O(α(n))。本节用 size。 |
复杂度 | 单次 merge = 两次 find + 常数。α(n) ≤ 4,见本节数学补给。 |
#include <bits/stdc++.h>
using namespace std;
int fa[10], sz[10];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
void merge(int x, int y) {
x = find(x); y = find(y);
if (x == y) return; // 已经在一起
if (sz[x] < sz[y]) swap(x, y); // 小的挂到大的下面
fa[y] = x;
sz[x] += sz[y];
}
int main() {
for (int i = 1; i <= 5; ++i) { fa[i] = i; sz[i] = 1; }
merge(1, 2); // {1,2}
merge(3, 4); // {3,4}
merge(2, 3); // 合成 {1,2,3,4}
cout << find(4) << " " << sz[find(1)] << "\n"; // 1 4
return 0;
}merge 内部一定会重新 find,所以调用时传原始点即可,别在外面再 find 一次(不报错,但容易忘)。
按大小合并防的是最坏情况:不加这一步,父指针的深度没有任何上界——数据可以把它串成一条长度为 n 的链,单次 find 从 O(1) 退化成 O(n)。
| 组成 | 含义 / 格式要求 |
|---|---|
struct Edge | 一条边存三个 int:u、v 是无向的两个端点,w 是权值。用结构体而不是三个平行数组,排序时整条边一起搬。 |
sort 的第三参数 | lambda,按 w 升序。必须写 < 不能写 <=——cmp 违反严格弱序会让 GCC 的 sort 越界访问,直接 RE。 |
e + 1, e + m + 1 | 1-indexed 的数组要用这两根指针。写成 e, e+m 会漏掉第 m 条边(代价是偶尔少一条最重边,答案偏小)。 |
cnt < n − 1 | 树只有 n−1 条边,凑够就停。它既是提前退出,也是最后判连通的依据。 |
find(u) == find(v) | 两端已连通 → 加这条边会成环 → 跳过。整个算法唯一做判断的地方。 |
merge(u, v) | 两端不在同一块,这条边把两个连通块缝起来了。并查集记下这次合并,后续判环就靠它。 |
ans += e[i].w | 只有真加进树的边才累加权值。必须写在 continue 之后。 |
结束条件 | cnt == n−1 说明成功;cnt < n−1 说明图不连通,此时 ans 只是“最小生成森林”的权值,不是答案。 |
#include <bits/stdc++.h>
using namespace std;
const int N = 10, M = 20;
struct Edge { int u, v, w; } e[M];
int fa[N], sz[N];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
void merge(int x, int y) {
x = find(x); y = find(y);
if (x == y) return;
if (sz[x] < sz[y]) swap(x, y);
fa[y] = x; sz[x] += sz[y];
}
int main() {
int n = 4, m = 5;
int raw[6][3] = {{0,0,0}, {1,2,2}, {1,3,2}, {1,4,3}, {2,3,4}, {3,4,3}};
for (int i = 1; i <= m; ++i) { e[i].u = raw[i][0]; e[i].v = raw[i][1]; e[i].w = raw[i][2]; }
sort(e + 1, e + m + 1, [](const Edge &a, const Edge &b) { return a.w < b.w; });
for (int i = 1; i <= n; ++i) { fa[i] = i; sz[i] = 1; }
long long ans = 0; int cnt = 0;
for (int i = 1; i <= m && cnt < n - 1; ++i) {
if (find(e[i].u) == find(e[i].v)) continue; // 成环,扔掉
merge(e[i].u, e[i].v);
ans += e[i].w; ++cnt;
}
cout << cnt << " " << ans << "\n"; // 3 7:加了 3 条边,权和 7
return 0;
}排序是最贵的一步:m = 2×10⁵ 时约 3.5×10⁶ 次比较,比并查集那几十万次操作重得多。想优化,先优化排序。
cnt 一定要在真加边时才 ++。写在 for 里或写在 continue 之前,会让“边数”算成 m,判连通永远成立 → 输出一个偏小的错答案。
给出一个包含 N 个结点的无向图,求它的最小生成树。
输入格式:第一行两个整数 N、M,表示 N 个结点、M 条无向边。
接下来 M 行,每行三个整数 Xᵢ、Yᵢ、Zᵢ,表示有一条长度为 Zᵢ 的边连接结点 Xᵢ 与 Yᵢ。
输出格式:如果图连通,输出一个整数,表示最小生成树各边长度之和;如果图不连通,输出 orz。
数据范围:1 ≤ N ≤ 5000,1 ≤ M ≤ 2×10⁵,1 ≤ Zᵢ ≤ 10⁴。
问题一:什么样的边集是树?n 个点、n−1 条边、连通。前两条用计数就能保证,第三条只能靠“加边时不成环”来保证——从 n 个孤立点开始,每加一条两端不在同一块的边,连通块数量恰好减 1。加满 n−1 条,块数变成 1。
问题二:怎么保证最小?用贪心:把边按权值升序,能加就加。正确性靠下面的割性质。
割性质(cut property)。把点集任意分成两块,取所有“一端在这块、一端在另一块”的边里最轻的一条 e,则存在一棵最小生成树包含 e。
证明:设 T 是不含 e 的最小生成树。T 中从 e 的一个端点到另一个端点有唯一路径,这条路径必然在某处跨出该块,设为边 f。则 T − f + e 仍是生成树,且 w(e) ≤ w(f),权值不增,故它也是最小生成树并含 e。∎
问题三:Kruskal 选的那条边,凭什么是“跨割最轻”的?设当前考察 eₖ,它的一端 u 在连通块 C、另一端不在。若有一条更轻的边 f 跨 (C, 外部),它一定在 eₖ 之前被处理过:f 若被加入,C 早已跨出去;f 若被跳过,说明 f 两端当时已连通,那另一端的点今天也该在 C 里。两种都矛盾。所以 eₖ 就是跨这个割最轻的边,由割性质它可以进 MST。
复杂度。排序 O(m log m) = 2×10⁵ × 17.6 ≈ 3.5×10⁶ 次比较;加边部分每条边至多两次 find,共 4×10⁵ 次、每次不超过 4 步。总计 10⁶ 量级,实测 20~40 ms。
不连通怎么办。加不满 n−1 条边。此时 ans 是“最小生成森林”的权值,题目要的是 orz,所以最后必须用 cnt 判一次,而不是直接输出 ans。
#include <bits/stdc++.h>
using namespace std;
const int N = 5005, M = 200005; // 点数与边数差 40 倍,千万别共用一个上界
struct Edge { int u, v, w; } e[M];
int fa[N], sz[N];
int find(int x) { // 路径压缩:找到根,顺便把路上的点挂到根上
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y) {
x = find(x); y = find(y);
if (x == y) return; // 已同集合:不需要改任何东西
if (sz[x] < sz[y]) swap(x, y); // 小树挂大树,保证树高只涨 1
fa[y] = x;
sz[x] += sz[y];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; ++i) cin >> e[i].u >> e[i].v >> e[i].w;
sort(e + 1, e + m + 1, [](const Edge &a, const Edge &b) { return a.w < b.w; });
for (int i = 1; i <= n; ++i) { fa[i] = i; sz[i] = 1; } // 并查集初始化
long long ans = 0;
int cnt = 0; // 已加入树的边数
for (int i = 1; i <= m && cnt < n - 1; ++i) { // 凑够 n-1 条就停
if (find(e[i].u) == find(e[i].v)) continue; // 两端已连通,加了会成环
merge(e[i].u, e[i].v);
ans += e[i].w;
++cnt;
}
if (cnt < n - 1) cout << "orz\n"; // 图不连通
else cout << ans << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
const int N = 5005, M = 200005; | 两个上界分开写。n ≤ 5000、m ≤ 2×10⁵,共用一个 5005 会读入越界 → RE。 |
struct Edge { int u, v, w; } e[M]; | 一条边三个 int。用结构体排序时整条边一起搬;用三个平行数组,sort 就没法直接排。 |
return fa[x] == x ? x : fa[x] = find(fa[x]); | 递归出口是 fa[x] == x;赋值号把压缩写进 return 里。少写等号左边那半,就退化成朴素查找。 |
if (sz[x] < sz[y]) swap(x, y); | 只比 size、不比值。这一行让最坏树高变成 log₂n,是“不压缩也不至于太慢”的保险。 |
sort(e + 1, e + m + 1, ...) | 按权值升序,这是全部贪心内容。cmp 用 < 而不是 <=,否则严格弱序被破坏 → 可能 RE。 |
for (int i = 1; i <= n; ++i) { fa[i] = i; sz[i] = 1; } | 初始化必须放在加边之前。写在读入之前也错——n 还没读进来。 |
long long ans = 0; | 本题 5000 个点、边权 10⁴,上界 5×10⁷,int 够;但求和一律 long long 是习惯,改题面时不用重想。 |
for (int i = 1; i <= m && cnt < n - 1; ++i) | 循环条件带 cnt:n−1 条边已经构成生成树,再看的边只可能更重。 |
if (find(e[i].u) == find(e[i].v)) continue; | 判环。这是并查集在本算法里唯一的用途,也是 Kruskal 比 Prim 好写的全部原因。 |
ans += e[i].w; ++cnt; | 两条都要在 continue 之后。顺序反了(cnt 先加)会让计数多 1,判连通永远成立。 |
if (cnt < n - 1) cout << "orz\n"; | 不连通时 ans 是森林的权值,直接输出它 → WA。输出 orz 才是题目要求的。 |
一、最小生成树唯一吗?不一定。所有边权互不相同时,MST 唯一;有相等边权时可能有多棵。反例:三个点两两相连、三条边权都是 1,删掉任意一条都是一棵 MST,共 3 棵,但它们的权值和都是 2。
所以做题时要分清题目问的是哪一个:问“最小代价”永远有唯一答案(Kruskal 直接输出);问“方案数 / 是否唯一”才需要额外处理,那属于生成树计数,超出本节范围。
为什么边权互不相同时一定唯一?假设有两棵不同的 MST,取它们的对称差,其中必有一条权值最小的边 e 属于 T₁ 不属于 T₂。把 e 加进 T₂ 形成环,环上必有一条不属于 T₁ 的边 f;由最小性 w(f) ≥ w(e),又因边权全不同只能 w(f) > w(e),于是 T₂ − f + e 更轻,与 T₂ 最小矛盾。∎
二、瓶颈路。定义 d(u,v) = 所有 u→v 路径中“最大边权”的最小值——走这条路,最宽的那道坎最窄。定理:对任意一棵 MST,树上 u→v 路径的最大边权就等于 d(u,v)。
这条定理的用途很直接:把“最小化最大值”的连通问题全部交给 MST。比如“用最小的最大边长把所有点连通”,答案就是 Kruskal 最后加进去的那条边的权值,连和都不用求——这正是 P2330 一类的题。
单次询问的做法:先求 MST,再从 u 出发 DFS 到 v,沿途记最大边权,O(n)。多组询问就要用倍增在树上跳(第 53 节),或把 Kruskal 的合并过程建成一棵重构树。本节只需记住结论。
cmp 写成 return a.w <= b.w;:违反严格弱序,GCC 的 sort 在数据稍大时越界访问 → RE,而且报错位置在 sort 里面,看起来跟你的代码无关,最难查。
漏写 if (find(u) == find(v)) continue;:每条边都被 merge、都计入 cnt,边数超过 n−1、ans 多加权值 → WA,并且程序不崩、不超时,只是答案偏大。
并查集初始化漏写或写错位置:fa 全是 0,所有点被判成同一集合 → 一条边都加不进去,cnt 恒为 0 → WA(输出 orz,或对 n = 1 才偶然正确)。
find 里省掉 fa[x] = ,同时 merge 又不按大小合并:父指针退化成一条长链,每次 find 走 O(n),2×10⁵ 条边 × 5000 步 = 10⁹ 次操作 → TLE。两个优化至少要留一个。
边数组按 n 开:n ≤ 5000 但 m ≤ 2×10⁵,只开 e[5005] 读入就写穿 → RE;开小了还能跑完时更可怕,表现为随机 WA。
不连通时直接输出 ans:题目要 orz,你输出了森林权值 → WA。养成“最后判 cnt == n−1”的习惯。
关掉页面,把 P3366 的 45 行从零敲一遍,用样例验证答案是 7。
改一改①:把 cmp 的 < 改成 <=,交一遍。小数据可能过,用 n = 5000、m = 2×10⁵ 的随机数据看 RE。
改一改②:删掉 if (find(...) == find(...)) continue;,只用样例跑一遍。正确答案 7,你会得到 11——多出来的是那条成环的边 4。
改一改③:把 merge 的两行换成 fa[find(x)] = find(y);(不按大小),同时把 find 里的 fa[x] = 删掉。用链式数据 (1,2)、(2,3)、…、(4999,5000) 再补 19 万条 (1,i) 边,观察 TLE;把两处改回来,同样的数据 0.02 s 跑完。
并查集的均摊复杂度既不是 log 也不是常数,而是一个增长极慢的函数:反阿克曼函数 α(n)。
阿克曼函数的前几项:A(1) = 2,A(2) = 4,A(3) = 16,A(4) = 65536,A(5) = 2⁶⁵⁵³⁶ ≈ 10¹⁹⁷²⁹。递推规则是 A(k+1) = 2A(k)——每一层把上一层的值整个塞进指数。
α(n) 就是“取多少个 A 才盖得住 n”:α(n) = min{ k : A(k) ≥ n }。
| n 的上界 | α(n) | 直观 |
|---|---|---|
| 4 | 2 | 课上演示用的规模 |
| 16 | 3 | 再大一点也只多一步 |
| 65536 | 4 | n = 5000 的 Kruskal 落在这一档 |
| 2⁶⁵⁵³⁶ ≈ 10¹⁹⁷²⁹ | 5 | 宇宙原子数约 10⁸⁰,离这档上界还差得远 |
结论:只要 n 小于 10¹⁹⁷²⁹,单次 find / merge 的均摊代价不超过 5 步,可以直接写成 O(1)。注意前提是路径压缩与按大小合并同时开:只开一个,上界是 O(log n);两个都不开,是 O(n)。
那 Kruskal 的时间花在哪?排序。m = 2×10⁵ 时 log₂m ≈ 17.6,比较次数约 m·log₂m = 3.5×10⁶;并查集部分只有约 2m = 4×10⁵ 次操作。排序占了九成以上。
这解释了两条实践结论:① 优化 Kruskal 先看排序,别抠 find 的常数;② 边权范围很小时(0/1,或 ≤ 10⁴ 的整数),把 sort 换成计数排序 / 桶排序,总复杂度压到 O(m + W),2×10⁵ 条边几乎是线性的。
fa[x] = 省掉之后会发生什么。国防部计划用无线网络连接若干个边防哨所。每台哨所都配一台无线电收发器,收发器的通信距离为 d:两个哨所之间的距离不超过 d 时才能用无线电直接通信。
现在有 S 台哨所可以额外加装卫星电话,卫星电话之间的距离不受任何限制(装了卫星电话的两台哨所永远可以通信)。
给出 S、P 和 P 个哨所的坐标,求能连通所有哨所的最小的 d。
输入格式:第一行两个整数 S、P(1 ≤ S ≤ 100,S ≤ P ≤ 500)。
接下来 P 行,每行两个整数 xᵢ、yᵢ(0 ≤ xᵢ, yᵢ ≤ 10000),表示第 i 个哨所的坐标。
输出格式:一行一个实数,表示 d 的最小值,保留两位小数。
卫星电话的作用不是“传得更远”,而是“允许整张网络断成 S 块”——块内必须靠无线电连起来。
先假想 d 已经确定:只保留长度 ≤ d 的边,你能算出图被分成几块吗?这个判定和 Kruskal 有什么关系?
把边按长度从小到大加,每加一条能连通两块的边,块数就减 1。数一数块数减到 S 时你加到第几条边。
把卫星电话翻译成连通块数。S 台卫星电话各自装在一个连通块里,块与块之间靠卫星通信,块内部必须靠无线电连通。所以最终恰好允许剩下 S 个连通块。分成更少块就浪费了电话,分成更多块则有一块没有电话、无法与外界通信。
问题变成:把 P 个点分成 S 块,使每块内部连通所需的无线电距离尽可能小,并对所有块取最大值。这正是“最小化最大边”的瓶颈问题,交给 MST。
Kruskal 恰好是一个不断减少连通块的过程。初始 P 块,每加入一条不成环的边,块数减 1。块数从 P 减到 S 需要 P − S 次合并,第 P − S 条被加入的边的长度就是答案——因为它正是“让块数刚好降到 S”的那道坎。
为什么不是第 P − S + 1 条?第 P − S 条加入后块数已经是 S,此时可以收手了;再加一条就把块数压到 S − 1,等于浪费了一台卫星电话,而那条边只会更长。
边界:S ≥ P 时每台哨所都能装卫星电话,不需要任何无线电,d = 0。不特判会让“凑够 P − S 条”永远不成立,ans 被更新到最大边。
复杂度。边数 P(P−1)/2 ≤ 124750,排序约 1.2×10⁵ × 17 ≈ 2×10⁶ 次比较,并查集十余万次操作,几十毫秒。
等价写法。也可以二分 d,判定“只用长度 ≤ d 的边能否把图连成 ≤ S 块”,复杂度 O(P² log),能过但多一个 log,而 Kruskal 一次就做完了。
#include <bits/stdc++.h>
using namespace std;
const int MAXP = 505, MAXE = MAXP * MAXP; // 完全图最多 P(P-1)/2 条边
struct Edge { int u, v; double w; } e[MAXE];
int fa[MAXP], sz[MAXP];
double x[MAXP], y[MAXP];
int find(int a) { return fa[a] == a ? a : fa[a] = find(fa[a]); }
bool merge(int a, int b) { // 返回 true 表示这次真的合并了两块
a = find(a); b = find(b);
if (a == b) return false; // 已连通,这条边会成环
if (sz[a] < sz[b]) swap(a, b);
fa[b] = a; sz[a] += sz[b];
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int S, P;
cin >> S >> P;
for (int i = 1; i <= P; ++i) cin >> x[i] >> y[i];
if (S >= P) { // 每台都装卫星电话,不需要无线电
cout << fixed << setprecision(2) << 0.0 << "\n";
return 0;
}
int m = 0;
for (int i = 1; i <= P; ++i) // 建完全图:任意两点都可能用无线电通信
for (int j = i + 1; j <= P; ++j) { // 从 i+1 开始,避免自环与重复边
double dx = x[i] - x[j], dy = y[i] - y[j];
e[++m] = {i, j, sqrt(dx * dx + dy * dy)};
}
sort(e + 1, e + m + 1, [](const Edge &a, const Edge &b) { return a.w < b.w; });
for (int i = 1; i <= P; ++i) { fa[i] = i; sz[i] = 1; }
int need = P - S; // 从 P 块压到 S 块,需要 P-S 次合并
int got = 0;
double ans = 0;
for (int i = 1; i <= m; ++i) {
if (!merge(e[i].u, e[i].v)) continue; // 成环,跳过
ans = e[i].w; // 记下这次合并用的边长
if (++got == need) break; // 凑够 P-S 条就停
}
cout << fixed << setprecision(2) << ans << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
const int MAXE = MAXP * MAXP; | 完全图有 P(P−1)/2 条边,P ≤ 500 时是 124750。开 P² 更宽松,也不用算半天。 |
bool merge(...) | 把 merge 改成返回 bool:返回 true 表示“这次真的合并了两块”,Kruskal 的计数就靠它。返回值比在外面再判一次 find 更省一次查找。 |
S >= P | 每台哨所一台卫星电话,d 可以取 0。不特判的话 need ≤ 0,++got == need 永远不成立,循环会一路加到最后一条边,ans 变成最大边长 → WA。 |
for (int j = i + 1; ...) | 只连 i < j 的点对。写成 j = 1 会多出一倍的重复边和 P 条自环(距离 0,会排在最前面,被 merge 判成同集合跳过,不致命但白排一遍)。 |
sqrt(dx * dx + dy * dy) | 欧氏距离。坐标差最大 10⁴,平方和最大 2×10⁸,double 有 53 位有效位,精度绰绰有余。 |
sort 按 w 升序 | Kruskal 的核心。用 double 排序时 cmp 仍写 <,别写 <=。 |
int need = P - S; | P 个点最终要剩 S 个连通块,每次成功合并让块数减 1,所以需要 P − S 条边。 |
ans = e[i].w; | 每次真合并都覆盖一次,最后一次覆盖的正是第 need 条边的长度。 |
if (++got == need) break; | 凑够就停。后面的边更长,不可能让答案更小。 |
fixed << setprecision(2) | 固定两位小数。用 cout 而不是 printf,避免和关闭同步的 cin 混用出问题。 |
++got == need 写成 got++ == need:先比较后自增,会多加一条边 → 答案偏大。cout << ans:评测要求 212.13 这样的格式 → WA。