题单 · 习题解析

洛谷 P2820 局域网

★★ 一句换算:「删掉的最大」= 总权和 − 「留下的最小」,而留下的必是**生成森林**;⚠⚠ 题面轻描淡写的两处 —— **没保证连通**(求森林别求树)、**`k` 的上限一个字没给**(别开死数组,也算不出 int 够不够);★ 反向 Kruskal「从大到小能删就删」**也对**(1200 / 1200)但慢 **28 倍**;★★★ 「打出留下的那个数」蒙对的 7 轮 ≡「删掉的恰好等于留下的」([P3959](/sol/p3959/) 那条的正面版,并列档放大到 38);★★ 不连通 **176** 组却只抓 **45** —— 131 组两版一起输出 0,**把中间那层数出来才对上**;★★★ 这道题**存不得邻接矩阵**(丢的是整条重边的权值,档 2 抓 300 / 300);⚠⚠ 自己踩的:生成器搭完树没记点对 ⇒ 档 0 混进重边、那一列本该是 0 却抓了 193

原题:洛谷 P2820出自 第 34 章 最小生成树:Kruskal 与 Prim 的题单题面本地存档:2026-08-31
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P2820,日期见页头。两边不一致时信原站。

题目背景

某个局域网内有 n 台计算机,由于搭建局域网时工作人员的疏忽,现在局域网内的连接形成了回路,我们知道如果局域网形成回路那么数据将不停的在回路内传输,造成网络卡的现象。因为连接计算机的网线本身不同,所以有一些连线不是很畅通,我们用 f(i,j) 表示 i, j 之间连接的畅通程度,f(i,j) 值越小表示 i, j 之间连接越通畅,f(i,j)0 表示 i, j 之间无网线连接。

题目描述

现在需要解决回路问题,我们将除去一些连线,使得网络中没有回路,不改变原图节点的连通性,并且被除去网线的 Σf(i,j) 最大,请求出这个最大值。

输入格式

第一行两个正整数 n, k

接下来的 k 行每行三个正整数 i, j, m 表示 i, j 两台计算机之间有网线联通,通畅程度为 m

输出格式

一个正整数,Σf(i,j) 的最大值。

说明/提示

对于全部数据,保证 1 ≤ n ≤ 1001 ≤ f(i,j) ≤ 1000

时限 1 秒,内存 125 MB。

输入输出样例

输入

5 5
1 2 8
1 3 1
1 5 3
2 4 5
3 4 2

输出

8

五条网线总畅通程度 8 + 1 + 3 + 5 + 2 = 19。 留下最通畅的一片森林(1-3 的 1、3-4 的 2、1-5 的 3、2-4 的 5,共 11), 删掉的就是 19 − 11 = 8 —— 正好是那条 1-2 的 8。

1★ 关键的一步:把「删掉的最大」翻成「留下的最小」

★★ 一句话换算,这道题就变成了本章第 6 步那份代码

题目要求删完之后满足两件事:没有回路连通性一个字都不变。 这两句话合起来只有一个意思 —— 留下的恰好是一片生成森林

于是:

删掉的 Σf 最大 ⟺ 留下的 Σf 最小 ⟺ 留下的是最小生成森林。

⇒ 答案 = 总权和 − 最小生成森林的权和本章第 6 步那份 Kruskal 一个字都不用改,只在最外面套一句减法。

p2820.cpp★ 这一版就能 AC(顶格 n = 100 完全图,本机 4 毫秒)
// P2820 局域网 —— ★ 这一版就能 AC
//
// ★ 关键的一步是一句话的换算:「**删掉的最大**」= 总权和 − 「**留下的最小**」。
// 题目要求删完之后「没有回路」+「不改变连通性」⇒ 留下的恰好是一片**生成森林**;
// 要删的最大,就让留下的最小 ⇒ 留下的是**最小生成森林**。
//
// ⚠⚠ 两处必须留神:
// ① 题面**没有保证图是连通的**(它说的是「不改变原图节点的连通性」)——
// 所以这里求的是**森林**不是树,Kruskal 一路收到底、不能要求「收够 n − 1 条」。
// ② 题面**一个字都没给 k 的上限**(只给了 `1 ≤ n ≤ 100` 和 `1 ≤ f ≤ 1000`)
// ⇒ 别按 n(n−1)/2 开死数组,用 vector 一路 push 就完了。
#include <bits/stdc++.h>
using namespace std;
int fa[105];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<array<int, 3>> e(k);
long long total = 0;
for (auto& t : e) {
cin >> t[1] >> t[2] >> t[0]; // 读进来是 i j m,存成 (m, i, j)
total += t[0]; // ★ 总权和要把**每一条**边都算上
}
sort(e.begin(), e.end());
for (int i = 1; i <= n; i++) fa[i] = i;
long long keep = 0;
for (auto& t : e) { // ⚠ 一路收到底,不数够不够 n − 1 条
int x = find(t[1]), y = find(t[2]);
if (x == y) continue;
fa[x] = y;
keep += t[0];
}
cout << total - keep << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠⚠ 而这句换算底下埋着两处,题面都说得很轻

① 题面从头到尾没说过图是连通的。 它说的是「不改变原图节点的连通性」—— 这句话恰恰是在提醒你: 原来断开的两块,删完之后也还得是断开的两块。 ⇒ 这里求的是森林不是树,Kruskal 得一路收到底,不能要求「收够 n − 1 条」

② 题面一个字都没给 k 的上限。 数据范围那一行只写了 1 ≤ n ≤ 1001 ≤ f(i,j) ≤ 1000 —— k 不在里面。 ★ 这是「读数据范围时先看每一档的主语是谁」的一个新形态: 这一次是那一行里少了一个数。 ⇒ 别按 n(n−1)/2 = 4950 开死数组,vector 一路 push 就完了。

2★ 另一条路:从大到小,能删就删

比起「换算成最小生成树」,还有一条更直白的路:把网线按畅通程度从大到小排, 一条条问「拆了它两头还连得上吗」,连得上就拆。

p2820Rev.cpp★ 反向 Kruskal —— 它也对(四档 1200 组一个不差)
// ★ P2820 的另一条路:从**大**到小扫,能删就删 —— 它也对
//
// 想法比换算更直白:把网线按畅通程度**从大到小**排,
// 一条一条问「拆了它之后,两头还连得上吗?」连得上就拆。
//
// ★ 它给出的答案和正解**逐组相同**(见本页第 ② 步,四档共 1200 组一个不差),
// 理由也是一句话:拆完之后留下的仍然是一片生成森林,
// 而「每次拆最贵的可拆边」正是 Kruskal 的**逆过程**(反向 Kruskal)。
// ⚠ 代价是它每问一次都要跑一遍连通性检查 —— O(k × (n + k)),
// 而正解是一次排序加一遍并查集。这道题 n ≤ 100 无所谓,规模一大就不行了。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<array<int, 3>> e(k);
long long total = 0;
for (auto& t : e) { cin >> t[1] >> t[2] >> t[0]; total += t[0]; }
sort(e.begin(), e.end(), greater<array<int, 3>>()); // 从大到小
vector<char> dead(k, 0);
/* 只用剩下的边,看 a 和 b 还连不连得上 */
auto connected = [&](int a, int b) {
vector<int> fa(n + 1);
for (int i = 1; i <= n; i++) fa[i] = i;
function<int(int)> f = [&](int x) { return fa[x] == x ? x : fa[x] = f(fa[x]); };
for (int i = 0; i < k; i++)
if (!dead[i]) { int x = f(e[i][1]), y = f(e[i][2]); if (x != y) fa[x] = y; }
return f(a) == f(b);
};
long long cut = 0;
for (int i = 0; i < k; i++) {
dead[i] = 1; // 先假装拆掉
if (connected(e[i][1], e[i][2])) cut += e[i][0];
else dead[i] = 0; // 拆了就断了 —— 放回去
}
cout << cut << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 它是对的,而且代价能量出来
反向 Kruskal 的答案 ≡ 正解 1200 / 1200(四个档各 300 组)
顶格 n = 100、完全图 4950 条边 · A 机 · WSL2 · 2026-08-31 · 独占 · 5 次取最小
 · 正解(一次排序 + 一遍并查集) 4 毫秒
 · 反向 Kruskal(每条边重跑一遍连通性) 111 毫秒 ⇒ ★ 28 倍

⇒ 这道题 n ≤ 10028 倍也照样过。 ★ 但它把「等价 ≠ 等值」说清楚了:两条路答案永远一样,代价差一个数量级 —— 和隔壁 P1195 那条(「先求完整 MST 再删」等价但一分钱不省)凑成一对: 「它也对」和「该写它」是两句话。

3★★★ 读反:输出「留下的」而不是「删掉的」

p2820Keep.cpp✗ 打出了留下的那个数(样例打出 11,当场挡住)
// ✗ P2820:把题目问的东西读反了 —— 输出「留下的最小」
//
// 题面问的是「被除去网线的 Σf(i,j) **最大值**」,而算出最小生成森林之后,
// 手边最现成的那个数是**留下的权和**。★ 顺手打出它,是这道题最常见的一发 WA。
// ⚠ 而它是个「每组都错」型的 bug ⇒ 官方样例当场挡住(打出 11,答案是 8)。
#include <bits/stdc++.h>
using namespace std;
int fa[105];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<array<int, 3>> e(k);
long long total = 0;
for (auto& t : e) { cin >> t[1] >> t[2] >> t[0]; total += t[0]; }
sort(e.begin(), e.end());
for (int i = 1; i <= n; i++) fa[i] = i;
long long keep = 0;
for (auto& t : e) {
int x = find(t[1]), y = find(t[2]);
if (x == y) continue;
fa[x] = y;
keep += t[0];
}
cout << keep << '\n'; // ✗ 问的是删掉的,不是留下的
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

算完最小生成森林之后,手边最现成的那个数是留下的权和 —— 顺手打出它,是这道题最常见的一发 WA。

★★★ 300 − 7 = 293:它蒙对的那几轮,是「删掉的」恰好等于「留下的」
300 轮 档 0 档 1 不连通 档 2 有重边 ★ 档 3 大量并列
「删掉的」恰好等于「留下的」 7 1 2 38
⇒ 「打出留下的」被抓 293 299 298 262

★★ 四个档全部一个不差300 − 触发数)。

⇒ 这是第 28 章 P3959 那条的正面版本: 「它算的是另一个量」推不出「它一定和正解不同」 —— 两个不同的量会在某些输入上取到同一个值,而那个「某些」只能数出来。 ★ 而档 3(畅通程度只取 1~3)把这个「某些」放大了 5 倍(7 → 38):值域一挤,撞车就多。

4⚠ 两个「生成器缺一档」的 0

p2820Conn.cpp✗ 以为图一定连通,非要收够 n − 1 条(样例照过)
// ✗ P2820:以为图一定连通,非要收够 n − 1 条
//
// 题面从头到尾**没说过图是连通的** —— 它说的是「不改变原图节点的连通性」,
// 这句话恰恰是在提醒你:原来断开的两块,**删完之后也还得是断开的两块**。
//
// ★ 这一版一旦发现收不满 n − 1 条,就当成「有问题」按 0 处理;
// ⚠ 而顺手写的生成器要是先造一棵树保证连通([P3366 那条](/sol/p3366/)),
// 这个 bug 又是**精确的 0**。
#include <bits/stdc++.h>
using namespace std;
int fa[105];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<array<int, 3>> e(k);
long long total = 0;
for (auto& t : e) { cin >> t[1] >> t[2] >> t[0]; total += t[0]; }
sort(e.begin(), e.end());
for (int i = 1; i <= n; i++) fa[i] = i;
long long keep = 0;
int cnt = 0;
for (auto& t : e) {
if (cnt == n - 1) break;
int x = find(t[1]), y = find(t[2]);
if (x == y) continue;
fa[x] = y;
keep += t[0];
cnt++;
}
if (cnt != n - 1) { cout << 0 << '\n'; return 0; } // ✗ 不连通就当没得删
cout << total - keep << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 176 组不连通,却只抓到 45 —— 中间那一层数出来就对上了
档 1(不搭那棵树)· 300 轮
第一层:原图真的不连通 176
 · 其中正解的答案本来就是 0(图本身就是森林,压根没边可删) 131
 · 其中正解的答案 ≠ 0 45
⇒ 「非要收够 n − 1 条」被抓 45

⇒ 那 131 轮里两版一起打出 0,对拍记「通过」而验的是零 (「一致有两种:都算对了,和都没算」)。

★★ 而这一段最值钱的动作是:抓获率对不上时别急着加轮数,去把中间那几层数出来B3625 那条)—— 数完之后 45 ≡ 45,一个不差

p2820Mat.cpp✗ 用 101 × 101 的邻接矩阵存网线(样例照过)
// ✗ P2820:用邻接矩阵存这些网线
//
// `n ≤ 100` ⇒ 一张 101 × 101 的矩阵又小又顺手,很多人第一反应就是它。
// ★★ 而这道题**存不得**:它要的不只是「留下哪些」,还要**总权和** ——
// 而矩阵一个格子只放得下一条边,**同一对计算机之间的第二条网线会被直接覆盖掉**。
//
// ⚠ 题面里既没有「保证没有重边」,`k` 的上限也一个字没提
// ⇒ 「同一对点之间有好几条线」是题面**允许**的输入。
// ★ 顺手写的生成器(在 n(n−1)/2 个点对里随机挑,不重复)**结构上**造不出重边
// ⇒ 那一档是精确的 0。
#include <bits/stdc++.h>
using namespace std;
int fa[105];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
int g[105][105];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
for (int i = 0; i < k; i++) {
int a, b, m;
cin >> a >> b >> m;
g[a][b] = g[b][a] = m; // ✗ 第二条线把第一条盖掉了
}
vector<array<int, 3>> e;
long long total = 0;
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++)
if (g[i][j]) { e.push_back({g[i][j], i, j}); total += g[i][j]; }
sort(e.begin(), e.end());
for (int i = 1; i <= n; i++) fa[i] = i;
long long keep = 0;
for (auto& t : e) {
int x = find(t[1]), y = find(t[2]);
if (x == y) continue;
fa[x] = y;
keep += t[0];
}
cout << total - keep << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 这道题存不得矩阵 —— 而理由和 [B3647] 那次不一样

n ≤ 100 ⇒ 一张 101 × 101 的矩阵才 40 KB,顺手得很。可它在这道题上是错的。

B3647 那次的「重边不取 min」,问题是留下的那条选错了; 这道题还要总权和,而矩阵一个格子只放得下一条边 ⇒ 同一对计算机之间的第二条网线连同它的权值一起消失了。

300 轮 档 0 档 1 ★★ 档 2 有重边 档 3
输入里真的有重边 0 0 300 0
⇒ 「用邻接矩阵存」被抓 0 0 300 0

★ 又一个一个不差。⚠ 而那三个 0 是生成器缺一档: 顺手写的生成器在「互不相同的点对」里挑边,结构上造不出重边。

⚠⚠ 而这一档我自己踩了一跤:第一版生成器搭完树之后没记住用掉的点对, 树边和额外边会撞成重边 —— 于是档 0 那一列本该是精确的 0,实测却抓了 193 次。 ⇒ 生成器注释里写的「这一档没有重边」,也要跑一遍才算数。

5★★★ 兑现本章第 10 步:两片森林可能不一样,权和必须相同

★★★ 「留下哪些边」不同 74 组,「权和」不同 0 组

拿 Kruskal 和一份 Prim 版最小生成森林(对每个连通块各长一次)逐组比:

300 组 档 0(畅通程度 1~30) ★ 档 3(只取 1~3,大量并列
两版留下的边集不同 8 74
两版权和不同 0 0

⇒ 和 P1546 那一页结论一致(那边是 6 / 39,这边是 8 / 74),而且并列一多就分岔

  • 别比边集
  • 答案那个数可以逐字节比 —— 这道题输出的恰好是那个不变量。

★ ⇒ 「对拍该比什么」的答案,写在题面的输出格式里B3644 那条的正面版本)。

6⚠ 题面自己打了一次架 —— 而那句话是噪声

★ 「f(i,j) 为 0 表示无网线连接」这句话,在这道题里一次都用不上

题目背景里写着:

f(i,j)0 表示 i, j 之间无网线连接。

输入格式说的是「接下来的 k 行每行三个正整数」,数据范围说的是 1 ≤ f(i,j) ≤ 1000

⇒ 三句话合起来:输入里根本不会出现 0 —— 图是按「k 条边」给的,不是按矩阵给的。 那句背景是在描述「概念上的 f」,不是在描述输入。

★ 按第 12 章那个判据称一称:造一档违反它的数据?造不出来 —— 1 ≤ f 已经把 0 挡死了。⇒ 它是噪声。 ⚠ 而这跟隔壁 P1195 恰好相反:那道题的数据范围明写着 0 ≤ L同一个「0」,一边是噪声,一边值一整个错法。

★ 要不要 long long:题面没给 k 的上限 ⇒ 这一次该保守
n = 100 的完全图(无重边)有 4950 条边
⇒ 总权和上界 4950 × 1000 = 4 950 000int 绰绰有余)
而要撑破 int 需要 2 147 483 条边

⇒ 只要 k 不超过两百万条,int 就够。可题面一个字都没保证 —— ⇒ 这是少见的「算不出确定答案」的情形:long long 是一次三十秒的保险,写上不亏。 ★ 对照前面那些页:那些题的界都能从题面乘出来,这道题乘不出来

7★ 对拍这一页

参照物是枚举「留下哪些网线」的所有子集:要求留下的那批 ① 没有回路 ② 连通性和原图一模一样, 在这样的子集里取权和最小的 —— 直接照题面那三句话走。

p2820Brute.cpp参照物:枚举留下哪些边(300 轮不一致 0 轮)
300 轮(n 随机 4~6) 档 0 默认 ★ 档 1 不连通 ★★ 档 2 有重边 ★ 档 3 大量并列
Kruskal(正解) 0 0 0 0
反向 Kruskal 0 0 0 0
打出留下的那个数 293 299 298 262
非要收够 n − 1 0 45 0 0
用邻接矩阵存 0 0 300 0
★★ 三个错法,三种不同的「触发条件 ↔ 抓获数」关系
错法 触发条件 触发几组 真被抓
用邻接矩阵存 输入里有重边(档 2) 300 300 1.0
打出留下的 「删掉的」≠「留下的」(档 0) 293 293 1.0
非要收够 n − 1 图不连通(档 1) 176 45 3.9

⇒ 前两个是能证的等价,第三个是两层的 —— 而第二层(正解答案 ≠ 0)数出来之后, 它同样是 45 ≡ 45,一个不差。 ★★ 所以「差 3.9 倍」不是这个 bug 的性质,是「我第一层写得太粗」的性质。

★ 官方样例这一轮只挡住了一个(三分之一)
错法 抓获率(最狠的那一档) 官方样例挡住了吗
打出留下的那个数 299 / 300 挡住了(打出 11)
用邻接矩阵存 300 / 300 放过了 —— 样例五条边、没有重边
非要收够 n − 1 45 / 300 放过了 —— 样例是连通的

⇒ 又一次:中间那个也是 每组都错型(在它的档上 300 / 300),样例照样放过 —— 因为那组数据在结构上问不出这个问题P1746 那条)。

8度量程序和生成器

p2820Count.cpp度量程序(本页所有数字都出自它)
p2820Gen.cpp(五个档位)数据生成器

9一页纸

★★ 关键的一步 「删掉的最大」= 总权和 − 「留下的最小」,而留下的必是生成森林
⚠⚠ 题面轻描淡写的两处 没保证连通(求森林别求树)/ k 的上限一个字没给(别开死数组)
★ 另一条路 反向 Kruskal「从大到小能删就删」也对(1200 / 1200),⚠ 但慢 28 倍
★★★ 293 ≡ 293 「打出留下的」蒙对的那 7 轮 ≡「删掉的恰好等于留下的」;并列档放大到 38
★★ 45 vs 176 不连通 176 组只抓 45 —— 131 组两版一起输出 0,把中间那层数出来才对上
★★★ 存不得矩阵 它丢的不只是「选错边」,是整条重边的权值(档 2 抓 300 / 300)
⚠⚠ 自己踩的 生成器搭完树没记点对 ⇒ 档 0 混进重边,那一列本该是 0 却抓了 193
★ 同一个「0」 这道题的 f = 0噪声1 ≤ f 挡死),隔壁 P11950 ≤ L 值一整个错法