0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1102,日期见页头。两边不一致时信原站。
题目背景
出题是一件痛苦的事情!相同的题目看多了也会有审美疲劳, 于是我舍弃了大家所熟悉的 A+B Problem,改用 A−B 了哈哈!
题目描述
给出一串正整数数列以及一个正整数 C,要求计算出所有满足 A − B = C 的数对的个数
(不同位置的数字一样的数对算不同的数对)。
输入格式
输入共两行。
第一行,两个正整数 N, C。
第二行,第 i 个数为 aᵢ,数字之间用一个空格隔开,共 N 个正整数,作为要求处理的那串数。
输出格式
一行,表示该串正整数中包含的满足 A − B = C 的数对的个数。
说明 / 提示
- 对于
75%的数据,1 ≤ N ≤ 2000。 - 对于
100%的数据,1 ≤ N ≤ 2×10⁵,0 ≤ aᵢ < 2³⁰,1 ≤ C < 2³⁰。
2017/4/29 新添数据两组。
输入输出样例
输入
4 1 1 1 2 3
输出
3
样例解释:C = 1,数列是 1 1 2 3。
A = 2 能配两个 B = 1(两个 1 在不同位置,算两对),A = 3 能配一个 B = 2 ——
一共 3 对。上面那段输出是仓库里的 p1102.cpp 真跑出来的。
1先把题读干净 —— 一个等式,三个陷阱
A − B = C 换个写法就是 A = B + C:对每个数当 B,去数「有多少个数等于 B + C」。
算法就这一句话。真正丢分的是另外三处:
"不同位置的数字一样的数对算不同的数对" -> 重复元素要成块地数 (第 5 步)
"1 <= N <= 2e5" -> 答案最大 1e10,int 装不下 (第 6 步)
"0 <= a_i < 2^30, 1 <= C < 2^30" -> a_i + C 会不会溢出? (第 4 步)
一、能过的写法不止一种(二分 / 双指针 / 哈希表),双指针不是「更正确」,是把 log 抹掉;
二、两个 bug,两种「对拍抓不到」:一个是概率问题(小值域才抓得到), 另一个是算术问题 —— 小数据上算术上不可能抓到。
2第 ① 版:两重循环 —— ★ 题面把它标价成 75 分
// P1102 的第 ① 版:两重循环,把每一对都试一遍//// ★ 它**不是废纸**:题面明写着「对于 75% 的数据,1 <= N <= 2000」——// N = 2000 时只有 200 万对,稳过。**考场上就是实打实的 75 分。**//// ⚠ 满数据 N = 2×10⁵ 时是 2×10¹⁰ 对,没有任何机会。//// ⚠ 就算是这一版,ans 也必须开 long long —— 溢出和快慢是两件独立的事,// 一个「只能拿 75 分」的暴力,也可能在那 75% 里因为 int 溢出而变成 0 分。// (这道题的部分分数据 N <= 2000 ⇒ 最大 10⁶ 对,int 够用;但这一笔要算过才知道。)
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, c; cin >> n >> c; vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; long long ans = 0; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) if (a[i] - a[j] == c) ans++; // A = a[i], B = a[j] printf("%lld\n", ans); return 0;}点「运行 ▶」看结果
N = 2000 时只有 400 万对,0.00 秒。⇒ 第 45 章那条考场策略在这道题上
写得比哪儿都清楚:不会正解的时候,暴力不是废纸,是 75 分。
满数据 N = 2×10⁵ 就是另一回事了 —— 4×10¹⁰ 对。
本机实测(B 机:原生 Ubuntu 7.0.0-30 / i5-10210U 8 线程 / 18 GB,2026-08-27,
顶格形状:一半是 x、一半是 x+C;⚠ 不是独占(机器上同时开着浏览器,load average 约 3)—— 秒数只当量级看,倍数才是重点):
N |
对数 | 秒表 |
|---|---|---|
| 5 000 | 2.5×10⁷ | 0.00 秒 |
| 10 000 | 1.0×10⁸ | 0.02 秒 |
| 20 000 | 4.0×10⁸ | 0.09 秒 |
| 200 000(满数据) | 4.0×10¹⁰ | 按 N² 外推约 9 秒 |
3第 ② 版:排序 + 二分 —— 最容易写对的那一版
排好序之后,等于 B + C 的那些数连成一块,用两个 STL 函数就能量出这块有多长:
upper_bound(a, B + C) - lower_bound(a, B + C)
// P1102 的第 ② 版:排序 + 二分(STL 的 lower_bound / upper_bound)//// 排好序之后,等于 a[i] + C 的那一块可以直接二分出来://// upper_bound(a, a[i] + C) - lower_bound(a, a[i] + C)//// O(N log N),满数据 2×10⁵ 稳过。★ 它比双指针**更容易写对**:// 不用想「指针要不要回退」,也不用想重复元素怎么数 —— 两个 STL 函数把边界替你管了。//// ⇒ 这一页想说的一句话:**双指针是把二分的 log 抹掉,不是「更正确」。**// 考场上先写你最有把握的那个;这道题 N log N 和 N 都在时限里,二分完全够。//// ⚠ a[i] + C 会不会溢出 int:a_i、C 都小于 2³⁰ ⇒ 和最大 2 147 483 646,// 而 int 上限 2 147 483 647 —— **只差 1**。见 p1102Count.cpp 那笔账。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, c; cin >> n >> c; vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; sort(a.begin(), a.end()); long long ans = 0; for (int i = 0; i < n; i++) { int want = a[i] + c; // B = a[i],要找的 A ans += upper_bound(a.begin(), a.end(), want) - lower_bound(a.begin(), a.end(), want); } printf("%lld\n", ans); return 0;}点「运行 ▶」看结果
★ 它 O(N log N),满数据 7.8 ms,稳过。而且重复元素不用操心 ——
upper_bound − lower_bound 天生就是「这一块有几个」。
4第 ③ 版:排序 + 双指针 —— 这一章的写法
a[i] 单调不减 ⇒ 要找的 a[i] + C 也单调不减 ⇒ 那一块只会往右挪,两个指针都不用回退。
j 停在第一个 a[j] - a[i] >= C 的位置 这一块的左端
k 停在第一个 a[k] - a[i] > C 的位置 这一块的右端 + 1
ans += k - j
// P1102 A-B 数对 —— 能 AC 的那一版:排序 + 双指针//// A − B = C 等价于 A = B + C。排好序之后,对每个 B = a[i],// 所有等于 a[i] + C 的元素在数组里是**连成一块**的,只要知道这块的两端就行。//// 两个指针都只往右走,一趟扫完:// j 停在第一个 a[j] − a[i] >= C 的位置 (这一块的左端)// k 停在第一个 a[k] − a[i] > C 的位置 (这一块的右端 + 1)// 这一块有 k − j 个 ⇒ ans += k − j//// ★ 为什么 j、k 不用回退:a[i] 单调不减 ⇒ a[i] + C 也单调不减 ⇒ 那一块只会往右挪。// 这就是第 7 章第 3 步那条前提在「排好序的数组」上的样子。//// ★★ 用「减」不用「加」:这里写的是 `a[j] - a[i] < C`,不是 `a[j] < a[i] + C`。// 两种写法在这道题上**都不会溢出**,但那是算出来的,不是碰运气 ——// a_i 和 C 都小于 2³⁰,和最大 2 147 483 646,而 int 上限 2 147 483 647。// ⇒ **富余量正好是 1。**(这笔账见 p1102Count.cpp,正文第 4 步专门讲了它。)//// ⚠ ans 必须是 long long:N = 2×10⁵ 时最大 10¹⁰(见 p1102Int.cpp)。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, c; cin >> n >> c; vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; sort(a.begin(), a.end());
long long ans = 0; int j = 0, k = 0; for (int i = 0; i < n; i++) { while (j < n && a[j] - a[i] < c) j++; // 左端:第一个差 >= C while (k < n && a[k] - a[i] <= c) k++; // 右端:第一个差 > C ans += k - j; } printf("%lld\n", ans); return 0;}点「运行 ▶」看结果
题面给的是 aᵢ < 2³⁰ 和 C < 2³⁰,所以
a_i + C 最大 1073741823 + 1073741823 = 2147483646
int 上限 2147483647
富余量 1它不溢出 —— 但只差一个数。 题面要是把上界写成「aᵢ ≤ 2³⁰」(含等号),
a[i] + C 当场翻车,而样例和所有小数据依然全对。
⇒ 上面那份用的是减法(a[j] - a[i] < C),根本不构造 a[i] + C,天然躲开这一条;
第 ② 版和第 ⑤ 版用的是加法,靠的是这笔账算过了。
★ 「算过了确认不用」和「没算过」是两回事 —— 这是第 45 章反复说的那句。
⚠ 对拍的档位 2 专门把 a_i + C 顶到 2 147 483 646 跑三百轮,
就是让这笔账被真的跑过一遍,而不是只写在注释里。
5★★★ 第 ④ 版(错的):重复元素只数了一个
// 演示错误写法:双指针只数了「第一个」匹配的元素 —— 重复元素全漏了//// 题面那句「**不同位置的数字一样的数对算不同的数对**」就是冲这个来的:// a = [1, 1, 2, 3]、C = 1 时,A = 2 能配两个 B = 1,答案是 3 不是 2。//// ★ 这一版只找到那一块的左端就 ans++,等于把整块当成一个。// ⇒ 只要数据里有重复元素就会漏,**而重复元素在小值域的随机数据里满地都是**。// 这也是第 7 章第 12 步刚踩过的坑(对撞指针数对时的「成块数」)。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, c; cin >> n >> c; vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; sort(a.begin(), a.end()); long long ans = 0; int j = 0; for (int i = 0; i < n; i++) { while (j < n && a[j] - a[i] < c) j++; if (j < n && a[j] - a[i] == c) ans++; // ← 只数了一个,整块只算一次 } printf("%lld\n", ans); return 0;}点「运行 ▶」看结果
题面那句「不同位置的数字一样的数对算不同的数对」就是冲它来的:
a = [1, 1, 2, 3]、C = 1 时 A = 2 能配两个 B = 1。
这个 bug 只在同一个值出现两次以上时露头。三个对拍档位的实测:
| 对拍档位 | 形状 | N / 值域 |
300 轮抓到 |
|---|---|---|---|
| 档位 0 | N ≤ 12、aᵢ ≤ 20、C ≤ 10 |
约 0.6 | 48 |
| 档位 1 | N ≤ 200、aᵢ ≤ 30、C ≤ 15 |
约 6.5 | ★ 286 |
| 档位 2 | N ≤ 12、aᵢ 和 C 都贴着 2³⁰(只有 5 个取值) |
约 2.4 | 98 |
★★ 档位 0 的值域已经很小了(0..20),可它只抓到 48 / 300 ——
因为 N 也只有十来个,平均每个值连一次都摊不上。
档位 1 把 N 放大到 200、值域几乎没变,密度一下到 6.5,就变成了 286 / 300。
⇒ 结论要说准:决定覆盖能力的是「平均每个值出现几次」,也就是 N / 值域,
单说「把数据造小」是不够的 —— 档位 0 就是个反例,它两头都小,反而最差。
⚠ 而照题面规模随机(aᵢ 在 [0, 2³⁰) 上均匀取)密度是 2×10⁻⁴,
2×10⁵ 个数落在十亿个格子里,重复元素几乎不出现,这个 bug 一轮也抓不到。
⇒ 第 6 章 P1719 那条「小本身就是覆盖能力」在这里被说细了一层。
6★★★ 第 ⑤ 版(错的):ans 用了 int —— 而触发它需要一条精确到 1 的线
答案能有多大?一半的数是 x、另一半是 x + C 时最大,等于 ⌊N/2⌋ × ⌈N/2⌉。
N = 2×10⁵ 时是 10¹⁰,int 差得远。
⌊N/2⌋ × ⌈N/2⌉ > 2 147 483 647 第一次成立,正好在 N = 92 682:
N |
答案能到的最大值 | int |
|---|---|---|
| 92 681 | 2 147 441 940 |
还差 41 707 就到 —— 装得下 |
| 92 682 | 2 147 488 281 |
★ 越线 |
真跑一遍(p1102GenBig 造顶格形状):
N |
long long 版 |
int 版 |
|---|---|---|
| 92 681 | 2147441940 |
2147441940(一模一样) |
| 92 682 | 2147488281 |
★ -2147479015 |
| 200 000 | 10000000000 |
1410065408 |
⇒ 对拍的小数据(N ≤ 200)一辈子也抓不到它,而且原因不是运气:
100 × 100 = 10⁴,离 2³¹ 差五个数量级。
生成器够不够,是一道算术题(第 6 章 P3406 那条)。
★ 所以这一条不靠对拍,靠的是把线算出来,然后在线的两边各跑一次。
check:viz 钉的就是上面那张表。
7第 ⑥ 版:连排序都不用 —— 哈希表计数
// P1102 的第 ③ 版:连排序都不用 —— 哈希表计数,O(N)//// 先把每个值出现了几次记进哈希表,然后对每个 B = a[i] 查一次 cnt[a[i] + C]。//// ★ 它和双指针一样是 O(N),而且**根本不依赖顺序** ——// 这说明这道题的「双指针」其实是可选的一条路,不是唯一解。//// ⚠ 但常数差得远:哈希表每次查询都要算哈希、可能碰撞、访问的内存到处乱跳;// 双指针那一版从头到尾只是顺着数组走。正文第 7 步那张表量了这个差别。//// ⚠ unordered_map 在有心人构造的数据上会被卡成 O(N²)(哈希碰撞攻击),// 洛谷这道题的数据没有这么干,但**知道有这回事**很重要。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, c; cin >> n >> c; vector<int> a(n); unordered_map<int, int> cnt; cnt.reserve(n * 2); for (int i = 0; i < n; i++) { cin >> a[i]; cnt[a[i]]++; } long long ans = 0; for (int i = 0; i < n; i++) { auto it = cnt.find(a[i] + c); if (it != cnt.end()) ans += it->second; } printf("%lld\n", ans); return 0;}点「运行 ▶」看结果
它 O(N),而且根本不依赖顺序 —— 这说明「双指针」在这道题上是可选的一条路,不是唯一解。
unordered_map 在有心人构造的数据上会被哈希碰撞卡成 O(N²)。
洛谷这道题的数据没有这么干,但知道有这回事很重要 ——
比赛里被卡 unordered_map 是常见死法(换 map,或者给键加一个随机扰动)。
8★ 对拍:900 轮,三档
// 数据生成器(P1102 对拍用):`./p1102Gen <seed> [level]`//// level 0(默认)n <= 12、a_i <= 20、C <= 10 —— **值域故意压得很小,重复元素满地都是**// level 1 n <= 200、a_i <= 30、C <= 15 —— 重复更密,答案更大// level 2 **顶格值域**:C 贴着 2³⁰,值只取 {0, 1, C-1, C, 2³⁰-1}//// ★ level 0 / 1 是给 p1102One(漏掉重复元素)准备的:// 值域压小 ⇒ 重复必然出现 ⇒ 那个 bug 每一轮都露头。// (第 6 章 P1719 那条:**对拍的小数据不是「凑合」,小本身就是覆盖能力。**)//// ★★ level 2 是给「a_i + C 会不会溢出」那笔账准备的:// 它把 a_i + C 顶到 2 147 483 646 —— 离 int 的上限只差 1。// 这一档三百轮全一致,就是那笔账**被跑过一遍**的证据。//// ⚠ 这个生成器**造不出**答案溢出的数据:那需要 N >= 92 682(见 p1102Count.cpp)。// ⇒ p1102Int.cpp 那个 bug 在这里是「算术上不可能」抓到的,不是「概率低」。
#include <bits/stdc++.h>using namespace std;
static mt19937 rng;static int ri(int lo, int hi) { return lo + (int)(rng() % (unsigned)(hi - lo + 1)); }
int main(int argc, char** argv) { unsigned seed = (argc > 1) ? (unsigned)atoi(argv[1]) : 1; int level = (argc > 2) ? atoi(argv[2]) : 0; rng.seed(seed);
if (level == 2) { int n = ri(2, 12); int c = (1 << 30) - ri(1, 10); printf("%d %d\n", n, c); int pool[5] = {0, 1, c - 1, c, (1 << 30) - 1}; for (int i = 0; i < n; i++) printf("%d%c", pool[ri(0, 4)], i == n - 1 ? '\n' : ' '); return 0; } int n = (level == 1) ? ri(2, 200) : ri(1, 12); int hi = (level == 1) ? 30 : 20; int c = (level == 1) ? ri(1, 15) : ri(1, 10); printf("%d %d\n", n, c); for (int i = 0; i < n; i++) printf("%d%c", ri(0, hi), i == n - 1 ? '\n' : ' '); return 0;}点「运行 ▶」看结果
// 大数据生成器(P1102 计时 / 溢出用):`./p1102GenBig <n> [level] [C]`//// level 0(默认)**顶格形状**:一半是 x、一半是 x + C ⇒ 答案 = floor(n/2) × ceil(n/2),// 这是这道题答案能到的最大值,也是 int 唯一会翻车的形状// level 1 随机:a_i 在整个 [0, 2³⁰) 上均匀取 ⇒ 答案几乎必然是 0//// ★ 两个档位的对比就是「顶格 ≠ 随机」:同样是 n = 2×10⁵,// 档位 0 的答案是 10¹⁰,档位 1 的答案大概率是 0 ——// **拿档位 1 去测「答案会不会溢出」,测一辈子也测不出来。**
#include <bits/stdc++.h>using namespace std;
static mt19937 rng;
int main(int argc, char** argv) { int n = (argc > 1) ? atoi(argv[1]) : 200000; int level = (argc > 2) ? atoi(argv[2]) : 0; int c = (argc > 3) ? atoi(argv[3]) : 1; rng.seed(12345); printf("%d %d\n", n, c); vector<int> v(n); for (int i = 0; i < n; i++) v[i] = (level == 0) ? ((i < n / 2) ? 1000 : 1000 + c) : (int)(rng() & ((1u << 30) - 1)); shuffle(v.begin(), v.end(), rng); // 打乱:别让排序捡到「已经有序」的便宜 for (int i = 0; i < n; i++) printf("%d%c", v[i], i == n - 1 ? '\n' : ' '); return 0;}点「运行 ▶」看结果
check:viz 每档 300 轮,暴力当标准答案 —— 三档 900 轮,四个能过的版本逐字节相同;
两个错版被抓到的轮数见上面那两张表。
9六个版本并排
// 三笔账 + 一把秒表//// 用法:./p1102Count <N> 人话版// ./p1102Count <N> csv 只打 `键,值`,给 check:viz 用//// ① 尺子:暴力要试 N² 对;排序 + 双指针只扫 3N 次(一次排序 + 两个指针各走一趟)。// ② 溢出账之一(**答案**):一半是 x、一半是 x + C 时答案最大,// 等于 floor(N/2) × ceil(N/2)。它从哪个 N 开始越过 int?—— 这是**算出来**的一条线。// ③ 溢出账之二(**中间量**):a_i < 2³⁰、C < 2³⁰ ⇒ a_i + C 最大 2 147 483 646,// 而 int 上限 2 147 483 647。★★ **富余量正好是 1。**//// ⚠ ③ 这一笔是这道题最险的地方:它**不溢出**,但只要题面把 a_i 的上界写成 2³⁰(含)// 而不是「小于 2³⁰」,`a[i] + C` 就当场翻车。⇒ 「算过了确认不用」和「没算过」是两回事。
#include <bits/stdc++.h>using namespace std;
static double now_ms() { timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec * 1000.0 + t.tv_nsec / 1e6;}
int main(int argc, char** argv) { long long N = (argc > 1) ? atoll(argv[1]) : 200000; bool csv = (argc > 2 && string(argv[2]) == "csv");
long long intMax = 2147483647LL; long long brute = N * N; long long two = 3 * N;
long long ansMax = (N / 2) * (N - N / 2); // 一半 x、一半 x+C long long lineN = 0; // 第一个让 ansMax 越过 int 的 N for (long long k = 2; ; k++) { if ((k / 2) * (k - k / 2) > intMax) { lineN = k; break; } } long long belowMax = ((lineN - 1) / 2) * ((lineN - 1) - (lineN - 1) / 2); long long lineMax = (lineN / 2) * (lineN - lineN / 2);
long long aMax = (1LL << 30) - 1; // 题面:a_i < 2^30 long long cMax = (1LL << 30) - 1; // 题面:C < 2^30 long long sumMax = aMax + cMax; long long margin = intMax - sumMax;
/* 秒表:顶格形状(一半 x、一半 x+C)上三种能过的写法 */ int n = (int)N, c = 1; vector<int> a(n); for (int i = 0; i < n; i++) a[i] = (i < n / 2) ? 1000 : 1000 + c;
vector<int> b = a; double t0 = now_ms(); sort(b.begin(), b.end()); long long ansTwo = 0; { int j = 0, k = 0; for (int i = 0; i < n; i++) { while (j < n && b[j] - b[i] < c) j++; while (k < n && b[k] - b[i] <= c) k++; ansTwo += k - j; } } double tTwo = now_ms() - t0;
b = a; t0 = now_ms(); sort(b.begin(), b.end()); long long ansBin = 0; for (int i = 0; i < n; i++) { int want = b[i] + c; ansBin += upper_bound(b.begin(), b.end(), want) - lower_bound(b.begin(), b.end(), want); } double tBin = now_ms() - t0;
t0 = now_ms(); unordered_map<int, int> cnt; cnt.reserve(n * 2); for (int i = 0; i < n; i++) cnt[a[i]]++; long long ansMap = 0; for (int i = 0; i < n; i++) { auto it = cnt.find(a[i] + c); if (it != cnt.end()) ansMap += it->second; } double tMap = now_ms() - t0;
if (csv) { printf("N,%lld\nbrute,%lld\ntwo,%lld\nratio,%lld\n" "ansMax,%lld\nansFitsInt,%d\nlineN,%lld\nbelowMax,%lld\nlineMax,%lld\n" "aMax,%lld\ncMax,%lld\nsumMax,%lld\nintMax,%lld\nmargin,%lld\n" "sameAns,%d\n", N, brute, two, brute / two, ansMax, ansMax <= intMax ? 1 : 0, lineN, belowMax, lineMax, aMax, cMax, sumMax, intMax, margin, (ansTwo == ansBin && ansTwo == ansMap) ? 1 : 0); } else { printf("N = %lld\n\n", N); printf("(1) 两重循环 %14lld 次\n", brute); printf("(2) 排序 + 双指针 %14lld 次 => 差 %lld 倍\n\n", two, brute / two); printf("答案的上界(一半 x、一半 x+C):%lld => int %s\n", ansMax, ansMax <= intMax ? "够" : "不够"); printf(" 越线的 N 是 %lld:N = %lld 时最大 %lld(不到),N = %lld 时 %lld(越过)\n\n", lineN, lineN - 1, belowMax, lineN, lineMax); printf("中间量 a_i + C 的上界:%lld + %lld = %lld\n", aMax, cMax, sumMax); printf("int 的上限 %lld => 富余量 %lld\n\n", intMax, margin); printf("顶格形状上的秒表(答案 %lld):\n", ansTwo); printf(" 排序 + 双指针 %8.1f ms\n", tTwo); printf(" 排序 + 二分 %8.1f ms\n", tBin); printf(" 哈希表计数 %8.1f ms\n", tMap); } return 0;}点「运行 ▶」看结果
满数据 N = 2×10⁵、顶格形状(同机同日,秒表是在一个进程里量的,不含读入;
⚠ 不是独占(机器上同时开着浏览器,load average 约 3)—— 秒数只当量级看,倍数才是重点):
| 版本 | 想法 | 复杂度 | 秒表 | 分数 |
|---|---|---|---|---|
① p1102Brute |
每一对都试 | O(N²) |
外推 9 秒 | ★ 75 分 |
② p1102Bin |
排序 + 二分 | O(N log N) |
7.8 ms | ★ 100 |
③ p1102 |
排序 + 双指针 | O(N log N) 排序主导 |
★ 2.2 ms | ★ 100 |
⑥ p1102Map |
哈希表计数 | O(N) |
4.6 ms | ★ 100 |
④ p1102One |
③ 少数了重复 | — | — | ✗ WA |
⑤ p1102Int |
③ 的 ans 是 int |
— | — | ✗ WA(N ≥ 92682 才现形) |
★ 三个能过的版本差不到 4 倍,都在时限里 —— 所以这道题真正的门槛从来不是快慢,
是第 5、6 步那两个 WA。
- ★ 能过的路有三条(二分 / 双指针 / 哈希)。
双指针不是「更正确」,只是把二分的
log抹掉;考场上先写你最有把握的那一版。 - ★★★ 两个 bug,两种「对拍抓不到」:
重复元素那个,要平均每个值出现几次够大才抓得到(档位 1 密度 6.5 → 286/300,
而档位 0 两头都小、密度 0.6 → 只有 48/300,照题面规模随机则一次都抓不到);
int溢出那个,在小数据上算术不可能抓到 —— 线精确在N = 92 682,只能算出来再去踩。 - ★★ 中间量也要算账。
aᵢ + C最大2 147 483 646,int上限2 147 483 647—— 富余量正好是 1。这题它躲过去了,但躲过去是算出来的,不是运气。