0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2872,日期见页头。两边不一致时信原站。
题目描述
给定 n 个点的坐标,第 i 个点的坐标为 (xᵢ, yᵢ),这 n 个点编号为 1 到 n。给定 m 条边,第 i 条边连接第 uᵢ 个点和第 vᵢ 个点。现在要求你添加一些边,并且能使得任意一点都可以连通其他所有点。求添加的边的总长度的最小值。
输入格式
第一行两个整数 n, m 代表点数与边数。
接下来 n 行每行两个整数 xᵢ, yᵢ 代表第 i 个点的坐标。
接下来 m 行每行两个整数 uᵢ, vᵢ 代表第 i 条边连接第 uᵢ 个点和第 vᵢ 个点。
输出格式
一行一个实数代表添加的边的最小长度,要求保留两位小数,为了避免误差,请用 64 位实型变量进行计算。
说明/提示
对于 100% 的数据,1 ≤ n, m ≤ 1000,1 ≤ xᵢ, yᵢ ≤ 10⁶,1 ≤ uᵢ, vᵢ ≤ n。
时限 1 秒,内存 125 MB。
输入输出样例
输入
4 1 1 1 3 1 2 3 4 3 1 4
输出
4.00
四个点 (1,1) (3,1) (2,3) (4,3),已经有一条 1-4 的路(白送)。
还差把 2 和 3 连进来:1-2 长 2、3-4 长 2 ⇒ 添加的总长 4.00。
1★★ 关键的一步全在建图 —— 算法只是最后一句
| 题面给的 | 翻成图是什么 |
|---|---|
n 个点的坐标 |
★ 任意两点之间都可以修一条路 ⇒ 完全图,n(n−1)/2 = 499 500 条边 |
| 边长 | 欧几里得距离 √(dx² + dy²) |
m 条已经修好的路 |
★ 权值当 0 —— 它们不要钱,唯一的作用是先把两头连起来 |
| 「添加的边总长最小」 | ⇒ 一句最小生成树 |
★ 这就是本章题单里那句「建图比算法难」的意思:想明白上面这张表之后, 剩下的就是本章第 7 步那份代码。
// P2872 [USACO07DEC] Building Roads S —— ★ 这一版就能 AC//// ★★ 这道题**难的不是算法,是建图**(本章题单那句「建图比算法难」):// 题面只给了 n 个点的坐标和 m 条**已经修好**的路 ——// 而任意两点之间都可以修一条新路 ⇒ 这是一张**完全图**,n(n−1)/2 = 499 500 条边。// 已经修好的那 m 条,**权值当 0**(不用再花钱)⇒ 剩下的就是一句最小生成树。//// ⇒ 完全图 + n = 1000 ⇒ 本章第 12 步那张表里「**朴素 Prim 占优**」的那一档:// O(n²) = 10⁶,而 Kruskal 要先摊开 499 500 条边再排序。实测见本页第 ⑥ 步。//// ⚠⚠ 两处会咬人的:// ① 坐标 ≤ 10⁶ ⇒ `dx * dx` 最大 10¹² —— **int 装不下**(见 p2872Int.cpp);// ② 题面明写「请用 **64 位**实型变量进行计算」⇒ `double` 不是 `float`(见 p2872Float.cpp)。
#include <bits/stdc++.h>using namespace std;
const int N = 1005;int x[N], y[N];double g[N][N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) cin >> x[i] >> y[i];
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { long long dx = x[i] - x[j], dy = y[i] - y[j]; // ★ long long:10¹² 装不进 int g[i][j] = sqrt((double)(dx * dx + dy * dy)); }
for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u][v] = g[v][u] = 0.0; // ★ 已经修好的路,权值当 0 }
vector<char> in(n + 1, 0); vector<double> best(n + 1, 1e18); best[1] = 0; double sum = 0; for (int it = 0; it < n; it++) { int u = -1; for (int i = 1; i <= n; i++) if (!in[i] && (u < 0 || best[i] < best[u])) u = i; in[u] = 1; sum += best[u]; // ★ 加的是**距离**,不是距离的平方 for (int i = 1; i <= n; i++) if (!in[i] && g[u][i] < best[i]) best[i] = g[u][i]; }
printf("%.2f\n", sum); return 0;}点「运行 ▶」看结果
2★★★ 这道题最经典的一发 WA:dx * dx 用了 int
// ✗ P2872:算 dx * dx 的时候用了 int//// ★★★ 这是这道题最经典的一发 WA,而它是**一道三十秒的算术题**:// 题面写着 `1 ≤ xᵢ, yᵢ ≤ 10⁶` ⇒ `dx` 最大 999 999 ⇒ `dx * dx` 最大 **≈ 10¹²**,// 而 `int` 只到 2 147 483 647(≈ 2.1 × 10⁹)—— **差三个数量级。**//// ⚠ 分界线算得出来:`46340² = 2 147 395 600 < 2³¹`,`46341² = 2 147 488 281 > 2³¹`// ⇒ **只要有一个坐标差 ≥ 46 341,单是那一项就溢出了**// ([第 18 章 P1516](/sol/p1516/) 那条分界线,一字不差地又出现了一次)。// ⇒ 而顺手把坐标压到 1000 以内的生成器,**结构上**造不出溢出。
#include <bits/stdc++.h>using namespace std;
const int N = 1005;int x[N], y[N];double g[N][N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) cin >> x[i] >> y[i];
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { int dx = x[i] - x[j], dy = y[i] - y[j]; // ✗ int g[i][j] = sqrt((double)(dx * dx + dy * dy)); // ✗ 乘法在 int 里就已经绕回去了 }
for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u][v] = g[v][u] = 0.0; }
vector<char> in(n + 1, 0); vector<double> best(n + 1, 1e18); best[1] = 0; double sum = 0; for (int it = 0; it < n; it++) { int u = -1; for (int i = 1; i <= n; i++) if (!in[i] && (u < 0 || best[i] < best[u])) u = i; in[u] = 1; sum += best[u]; for (int i = 1; i <= n; i++) if (!in[i] && g[u][i] < best[i]) best[i] = g[u][i]; } printf("%.2f\n", sum); return 0;}点「运行 ▶」看结果
题面写着 1 ≤ xᵢ, yᵢ ≤ 10⁶ ⇒ dx 最大 999 999 ⇒ dx * dx 最大 ≈ 10¹²,
而 int 只到 2 147 483 647 —— 差三个数量级。
⚠ 而「多大才会炸」精确到个位:
46340² |
2 147 395 600 < 2³¹−1 ✓ |
int 上限 |
2 147 483 647 |
46341² |
2 147 488 281 > 2³¹−1 ✗ |
⇒ 只要有一个坐标差 ≥ 46 341,单是那一项就已经绕回去了。
★ 这条分界线和第 18 章 P1516 那次一字不差(那道题是 46340² < 2³¹ ≤ 46341²)——
⇒ 它不是这道题的性质,是 int 的性质,值得背下来。
| 300 轮 | ★ 档 0 坐标 ≤ 1000 | ★★ 档 1 坐标照题面顶格 |
|---|---|---|
真的有一对点使 dx² + dy² 撑破 int |
★ 0 | 300 |
| ⇒ 「用 int」被抓 | ★ 0 | ★ 300 |
★ 触发条件 ≡ 抓获数,四个档一个不差。 ⇒ 又一次「顶格是顶到题面的边上」:坐标压小不是「数据弱」,是结构上碰不到那条线。
3⚠ 题面自己提醒的那半句:请用 64 位实型
// ✗ P2872:用 float 存距离和累加//// 题面里那半句「**为了避免误差,请用 64 位实型变量进行计算**」,就是冲这一版来的。//// ★ 一道三十秒的算术题:答案上界 ≈ `999 × √2 × 10⁶` ≈ **1.41 × 10⁹**,// 而 `float` 只有 **24 位尾数**(约 7 位十进制有效数字)// ⇒ 在 10⁹ 这个量级上,它连**个位**都表示不了,更别说题目要的**两位小数**。// ⚠ 而坐标压小的生成器**几乎**看不见它 —— 实测 300 轮只抓到 **2** 次// (答案才三位数时 float 绰绰有余);坐标照题面顶格才是 **256 / 300**。// ⇒ 注意这个「几乎」:它不是[精确的 0](/sol/p3366/),是一个很小的正数 ——// **两种情况要用不同的办法救**(前者只能改结构,后者加轮数也行)。
#include <bits/stdc++.h>using namespace std;
const int N = 1005;int x[N], y[N];float g[N][N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) cin >> x[i] >> y[i];
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { long long dx = x[i] - x[j], dy = y[i] - y[j]; g[i][j] = (float)sqrt((double)(dx * dx + dy * dy)); // ✗ 存进 float 就掉精度了 }
for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u][v] = g[v][u] = 0.0f; }
vector<char> in(n + 1, 0); vector<float> best(n + 1, 1e30f); best[1] = 0; float sum = 0; for (int it = 0; it < n; it++) { int u = -1; for (int i = 1; i <= n; i++) if (!in[i] && (u < 0 || best[i] < best[u])) u = i; in[u] = 1; sum += best[u]; // ✗ 累加也在 float 里 for (int i = 1; i <= n; i++) if (!in[i] && g[u][i] < best[i]) best[i] = g[u][i]; } printf("%.2f\n", (double)sum); return 0;}点「运行 ▶」看结果
「为了避免误差,请用 64 位实型变量进行计算。」
答案上界 ≈ 999 × √2 × 10⁶ |
1 412 799 349(≈ 1.41 × 10⁹) |
float 的十进制有效数字 |
★ 约 7 位 |
double |
约 16 位 |
⇒ 在 10⁹ 这个量级上,float 连个位都表示不了,而题目要的是两位小数。
| 300 轮 | 档 0 坐标 ≤ 1000 | 档 1 坐标顶格 | 档 2 已有路多 | 档 3 顶格 + n = 6 |
|---|---|---|---|---|
| float 版和 double 版打出的字符串不同 | 2 | 256 | ★ 0 | 275 |
| ⇒ 「用 float」被抓 | ★ 2 | ★ 256 | ★ 0 | ★ 275 |
★ 又一个四档一个不差。 ⚠⚠ 而注意档 0 那个 2 —— 它不是精确的 0,是一个很小的正数。 ⇒ 「精确的 0 和掉了一百倍要用不同的办法救」: 前者只能改结构,后者加轮数也行。写「坐标压小就看不见它」是不准确的, 准确的说法是「几乎看不见(2 / 300)」。
4★★★ 同一个「用平方代替距离」的念头,一半对一半错
sqrt 是这份代码里最贵的一步,而 Prim 每一轮只是在比大小 ——
于是很自然会想:能不能干脆存距离的平方,省掉那 10⁶ 次开方?
// ⚠ P2872:**比较**用距离的平方(不开方),只在累加的时候才 sqrt —— 而它是**对的**//// ★★★ 想法很自然:`sqrt` 慢,而 Prim 每一步只是在**比大小** ——// 而 `a < b ⟺ a² < b²`(两边都非负)⇒ **比较用平方完全等价**。// ⇒ 实测和正解 300 组逐字节相同(本页第 ⑤ 步)。//// ⚠⚠ 但**只有比较能这么干**:`√a + √b ≠ √(a + b)` ——// 把累加也换成平方就当场错(见 p2872SumSq.cpp,300 / 300)。// ⇒ **同一个「用平方代替距离」的念头,一半是对的一半是错的**,// 而分界线正好落在「比较」和「求和」之间。
#include <bits/stdc++.h>using namespace std;
const int N = 1005;int x[N], y[N];long long g[N][N]; // ★ 存的是距离的**平方**
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) cin >> x[i] >> y[i];
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { long long dx = x[i] - x[j], dy = y[i] - y[j]; g[i][j] = dx * dx + dy * dy; }
for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u][v] = g[v][u] = 0; }
vector<char> in(n + 1, 0); vector<long long> best(n + 1, LLONG_MAX); best[1] = 0; double sum = 0; for (int it = 0; it < n; it++) { int u = -1; for (int i = 1; i <= n; i++) if (!in[i] && (u < 0 || best[i] < best[u])) u = i; // ★ 比较:平方就够 in[u] = 1; sum += sqrt((double)best[u]); // ★ 求和:必须先开方 for (int i = 1; i <= n; i++) if (!in[i] && g[u][i] < best[i]) best[i] = g[u][i]; } printf("%.2f\n", sum); return 0;}点「运行 ▶」看结果
// ✗ P2872:连**求和**也用了距离的平方,最后才开一次方//// ★ 它是上一份(p2872Sq.cpp,**对的**)只挪了一个括号的产物:// ✓ `sum += sqrt(best[u])` ← 每条边各自开方,再相加// ✗ `sum += best[u]` 最后 sqrt ← 先把平方加起来,再开一次方// 而 `√a + √b ≠ √(a + b)`(除非有一边是 0)⇒ **每组都错**。
#include <bits/stdc++.h>using namespace std;
const int N = 1005;int x[N], y[N];long long g[N][N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) cin >> x[i] >> y[i];
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { long long dx = x[i] - x[j], dy = y[i] - y[j]; g[i][j] = dx * dx + dy * dy; }
for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u][v] = g[v][u] = 0; }
vector<char> in(n + 1, 0); vector<long long> best(n + 1, LLONG_MAX); best[1] = 0; long long sum = 0; for (int it = 0; it < n; it++) { int u = -1; for (int i = 1; i <= n; i++) if (!in[i] && (u < 0 || best[i] < best[u])) u = i; in[u] = 1; sum += best[u]; // ✗ 把平方加起来了 for (int i = 1; i <= n; i++) if (!in[i] && g[u][i] < best[i]) best[i] = g[u][i]; } printf("%.2f\n", sqrt((double)sum)); // ✗ 最后才开一次方 return 0;}点「运行 ▶」看结果
| 成不成立 | 为什么 | |
|---|---|---|
| 比较用平方 | ★ 成立 | a < b ⟺ a² < b²(两边非负) |
| 求和用平方 | ✗ 不成立 | √a + √b ≠ √(a + b) |
⇒ 两份代码只差一个括号的位置:
✓ sum += sqrt(best[u]); // 每条边各自开方,再相加
✗ sum += best[u]; ... sqrt(sum) // 先把平方加起来,再开一次方| 300 轮 | 档 0 | 档 1 | ★ 档 2 已有路多 | 档 3 |
|---|---|---|---|---|
| 最小生成树里非零的边 ≥ 2 条 | 286 | 286 | ★ 0 | 300 |
| ⇒ 「求和也用平方」被抓 | ★ 286 | ★ 286 | ★ 0 | ★ 300 |
★★ 又一个四档一个不差,而且档 2 那个 0 是能证的:
非零边只有 0 条或 1 条时,√(Σw²) 恰好就等于 Σw —— 它蒙对了。
⚠ 而这一档里有 247 / 300 组答案干脆就是 0.00(已有的路把图连满了)——
⇒ 那些轮两版一起打出 0.00,又一次「一致有两种:都算对了,和都没算」。
5⚠ 已有的那 m 条路,忘了把它们设成 0
// ✗ P2872:已经修好的那 m 条路,按实际距离算了钱//// 题面问的是「**添加的**边的总长度的最小值」——// 已经存在的路是白送的,它们唯一的作用是**把两头的点先连起来**。// ★ 顺手把 m 条已有边当成普通边一起丢进图里,就等于替它们又付了一次钱。// ⚠ 而它只在「已有边真的会被选进最小生成树」时才现形 —— 见本页第 ④ 步那两层。
#include <bits/stdc++.h>using namespace std;
const int N = 1005;int x[N], y[N];double g[N][N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) cin >> x[i] >> y[i];
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { long long dx = x[i] - x[j], dy = y[i] - y[j]; g[i][j] = sqrt((double)(dx * dx + dy * dy)); }
for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; // ✗ 读了,但一个字都没用上 }
vector<char> in(n + 1, 0); vector<double> best(n + 1, 1e18); best[1] = 0; double sum = 0; for (int it = 0; it < n; it++) { int u = -1; for (int i = 1; i <= n; i++) if (!in[i] && (u < 0 || best[i] < best[u])) u = i; in[u] = 1; sum += best[u]; for (int i = 1; i <= n; i++) if (!in[i] && g[u][i] < best[i]) best[i] = g[u][i]; } printf("%.2f\n", sum); return 0;}点「运行 ▶」看结果
| 300 轮 | 档 0 | 档 1 | ★ 档 2 已有路多且专挑长边 | 档 3 |
|---|---|---|---|---|
| 已有的路真的被选进树 | 181 | 181 | ★ 300 | 193 |
| ⇒ 「没设成 0」被抓 | ★ 181 | ★ 181 | ★ 300 | ★ 193 |
★ 第四个「四档一个不差」。⇒ 这一页四个错法的触发条件,全都精确地等于它们的抓获数。
6★★ 兑现 [P1546] 那句话 —— 这道题才是 Prim 和 Kruskal 真的分开的地方
// ★ P2872 的另一条路:Kruskal —— 它也能 AC,但要先摊开 499 500 条边//// ⚠ 这道题正是[本章第 12 步](/ch/34-mst/)那张表真正分胜负的地方:// [P1546](/sol/p1546/) 的 n 只有 100(4950 条边,选谁都是 5 毫秒),// 这道题 n = 1000 ⇒ **499 500 条边**,摊开 + 排序的钱就看得见了。实测见本页第 ⑥ 步。
#include <bits/stdc++.h>using namespace std;
const int N = 1005;int x[N], y[N], fa[N];int find(int a) { return fa[a] == a ? a : fa[a] = find(fa[a]); }
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) cin >> x[i] >> y[i];
vector<vector<char>> had(n + 1, vector<char>(n + 1, 0)); vector<pair<int, int>> pre(m); for (auto& p : pre) { cin >> p.first >> p.second; had[p.first][p.second] = had[p.second][p.first] = 1; }
vector<tuple<double, int, int>> e; e.reserve((size_t)n * (n - 1) / 2); for (int i = 1; i <= n; i++) for (int j = i + 1; j <= n; j++) { long long dx = x[i] - x[j], dy = y[i] - y[j]; double w = had[i][j] ? 0.0 : sqrt((double)(dx * dx + dy * dy)); e.push_back({w, i, j}); }
sort(e.begin(), e.end()); for (int i = 1; i <= n; i++) fa[i] = i;
double sum = 0; int cnt = 0; for (auto& [w, a, b] : e) { int p = find(a), q = find(b); if (p == q) continue; fa[p] = q; sum += w; if (++cnt == n - 1) break; } printf("%.2f\n", sum); return 0;}点「运行 ▶」看结果
| A 机 · WSL2 · 8 核 / 7 G · 2026-08-31 · 独占 · 5 次取最小 | P1546(n = 100) |
★ 这道题(n = 1000) |
|---|---|---|
| 完全图边数 | 4 950 | ★ 499 500 |
| 朴素 Prim | 5 毫秒 | ★ 9 毫秒 |
| Kruskal | 5 毫秒 | 39 毫秒 ⇒ ★ 4.3 倍 |
| 结论 | ⚠ 选型不值钱 | ★ 选型值 4.3 倍 |
⇒ ★★ 「稠密图该用朴素 Prim」这句话的主语是 n,而 n 要够大它才值钱。
两道题连起来看,这句话的价格曲线就有两个点了。
★ 顺带把内存那笔账也做了(题面给 125 MB,两条路都放得下):
| Kruskal 的边数组(499 500 × 16 字节) | 7 MB |
Prim 的矩阵(1001 × 1001 × 8) |
7 MB |
⚠ 而「比较用平方」那一版和正解跑得一样快(9 毫秒)——
省掉的那 10⁶ 次 sqrt 在这个规模上一分钱都不值,
⇒ 选它的理由只能是「不掉精度」,不是「更快」。
7★★★ 实数输出的题,对拍能不能逐字节比 —— 这个要量
P2240 那一页留过一句狠话:实数输出的题,对拍不能逐字节比 ——
那道题随机 20 万组里有 1861 组(0.93%),「精确值四舍五入」和 printf("%.2f") 打出的串不同。
这道题的三种正确写法(朴素 Prim / Kruskal / 比较用平方)加法顺序都不一样, 浮点加法又不满足结合律 ⇒ 完全有理由担心最后两位小数会飘。所以量了一遍:
| 四个档 300 轮 × 三种正确写法 + 参照物,逐字节比 | ★ 0 组不同 |
顶格 n = 1000 另跑 10 组,Prim vs Kruskal |
★ 0 组不同 |
⇒ 这道题逐字节比是安全的 —— 而这句话是量出来的,不是推出来的。
★★ 两页凑成一对:「实数输出要不要带容差」不是一个能背的规矩,是一道每题都要重问的题。
⚠ 而两边的差别也说得清:P2240 的答案是个有理数、正好会落在 0.125 这种「二分正中间」上;
这道题的每一项都是 sqrt 出来的无理数,落在两位小数的正中间是零概率事件。
8★ 对拍这一页
300 轮(n 随机 4~6) |
档 0 坐标 ≤ 1000 | ★★ 档 1 坐标顶格 | ★ 档 2 已有路多 | ★ 档 3 顶格 + n = 6 |
|---|---|---|---|---|
| 朴素 Prim(正解) | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| Kruskal | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 比较用平方 | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
dx * dx 用 int |
★ 0 | ★ 300 | ★ 0 | ★ 300 |
| 用 float | 2 | 256 | ★ 0 | 275 |
| 求和也用平方 | 286 | 286 | ★ 0 | 300 |
| 已有边没设 0 | 181 | 181 | ★ 300 | 193 |
| 错法 | 触发条件 | 四档触发 | 四档被抓 |
|---|---|---|---|
dx * dx 用 int |
有一对点 dx² + dy² > 2³¹−1 |
0 / 300 / 0 / 300 | ★ 同上 |
| 用 float | float 和 double 打出的串不同 | 2 / 256 / 0 / 275 | ★ 同上 |
| 求和也用平方 | 树里非零边 ≥ 2 条 | 286 / 286 / 0 / 300 | ★ 同上 |
| 已有边没设 0 | 已有的路真被选进树 | 181 / 181 / 300 / 193 | ★ 同上 |
⇒ 四个都是能证的等价(两版只在那一件事上分家),所以「一个不差」不是巧合。 ⚠ 而别把它当定律 —— 同一页上隔壁 P1547 的重边就是 300 触发只抓 129, P1803 差 60 倍、B3637 差 150 倍。 ★★ 能不能写成「≡」,取决于你第一层写得够不够细,不取决于这条规律。
| 错法 | 抓获率(最狠的那一档) | 官方样例挡住了吗 |
|---|---|---|
| 求和也用平方 | 300 / 300 | ★ 挡住了(打出 2.83) |
| 已有边没设 0 | 300 / 300 | ★ 挡住了(打出 6.24) |
dx * dx 用 int |
300 / 300 | ⚠ 放过了 —— 样例坐标只到 4,离 46 341 差了四个数量级 |
| 用 float | 275 / 300 | ⚠ 放过了 —— 样例答案才 4.00,float 绰绰有余 |
⇒ 四个全是「在它自己那一档上 300 / 300」的类型,而样例只挡住两个 —— ★★ 又一次:样例挡不挡得住,主语是「那组数据的结构」,不是「这个 bug 明不明显」 (P1266 那条)。
9度量程序和生成器
10一页纸
| ★★ 关键的一步 | 全在建图:完全图 499 500 条边 + 已有的路权值当 0 ⇒ 剩下就是一句 MST |
| ★★★ 最经典的一发 WA | dx * dx 用 int —— 分界线 46 341,和 P1516 一字不差 |
| ⚠ 生成器 | 坐标压到 1000 ⇒ 那个 bug 是精确的 0;顶格才 300 / 300 |
| ★ 题面写着的 | 「请用 64 位实型」—— 答案量级 1.41 × 10⁹,float 只有 7 位有效数字 |
| ★★★ 一半对一半错 | 比较用平方成立(1200 轮逐字节相同)、求和用平方不成立 |
| ★★ 选型 | 这道题 Prim 9 ms / Kruskal 39 ms ⇒ 4.3 倍(P1546 上同一句话一分不值) |
| ★★★ 实数逐字节比 | 这道题安全(0 组不同),而 P2240 是 0.93% ⇒ 每题都要重问 |
| ★★★ 四个一个不差 | 四个错法的触发条件全部 ≡ 抓获数,四档全对 —— ⚠ 但这取决于第一层写得够不够细 |