题单 · 习题解析

洛谷 P1102 A-B 数对

两个 WA,两种「对拍抓不到」:重复元素靠把值域压小才抓得到,int 溢出的线精确在 N = 92682

原题:洛谷 P1102出自 第 7 章 双指针与滑动窗口 的题单出自 第 8 章 二分查找 的题单题面本地存档:2026-08-27
⚠ 先自己写一遍,再往下看

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

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 3A = 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 分

p1102Brute.cpp第 ① 版:每一对都试一遍
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 「对于 75% 的数据,1 ≤ N ≤ 2000」是白纸黑字的分数

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¹⁰ 外推约 9 秒

3第 ② 版:排序 + 二分 —— 最容易写对的那一版

排好序之后,等于 B + C 的那些数连成一块,用两个 STL 函数就能量出这块有多长:

upper_bound(a, B + C) - lower_bound(a, B + C)
p1102Bin.cpp第 ② 版:排序 + 二分(能 AC)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

★ 它 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.cpp第 ③ 版:排序 + 双指针(能 AC)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 中间量的那笔账:a_i + C 的富余量正好是 1

题面给的是 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★★★ 第 ④ 版(错的):重复元素只数了一个

p1102One.cpp演示错误写法:找到一个就 ans++
样例上它也输出 3 —— 恰好在这组数据上不亏。真正的差别要重复元素更密才看得出来。
// 演示错误写法:双指针只数了「第一个」匹配的元素 —— 重复元素全漏了
//
// 题面那句「**不同位置的数字一样的数对算不同的数对**」就是冲这个来的:
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面那句「不同位置的数字一样的数对算不同的数对」就是冲它来的: a = [1, 1, 2, 3]C = 1A = 2 能配两个 B = 1

★★★ 抓它靠的不是「N 小」,也不是「值域小」—— 是两者的比值

这个 bug 只在同一个值出现两次以上时露头。三个对拍档位的实测:

对拍档位 形状 N / 值域 300 轮抓到
档位 0 N ≤ 12aᵢ ≤ 20C ≤ 10 0.6 48
档位 1 N ≤ 200aᵢ ≤ 30C ≤ 15 6.5 286
档位 2 N ≤ 12aᵢ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 的线

p1102Int.cpp演示错误写法:算法一字不差,ans 是 int

答案能有多大?一半的数是 x、另一半是 x + C 时最大,等于 ⌊N/2⌋ × ⌈N/2⌉N = 2×10⁵ 时是 10¹⁰int 差得远。

★★★ 这不是「概率低」,是「算术上不可能」——线在 N = 92 682

⌊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第 ⑥ 版:连排序都不用 —— 哈希表计数

p1102Map.cpp第 ⑥ 版:unordered_map 计数(也能 AC)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

O(N),而且根本不依赖顺序 —— 这说明「双指针」在这道题上是可选的一条路,不是唯一解。

⚠ 但它有一条别人没有的风险

unordered_map有心人构造的数据上会被哈希碰撞卡成 O(N²)。 洛谷这道题的数据没有这么干,但知道有这回事很重要 —— 比赛里被卡 unordered_map 是常见死法(换 map,或者给键加一个随机扰动)。

8★ 对拍:900 轮,三档

p1102Gen.cpp生成器:三个档位
参数是「种子 档位」。档位 2 把 a_i + C 顶到离 int 上限只差 1 的地方。
// 数据生成器(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;
}
点一下即可编辑
输出
点「运行 ▶」看结果
p1102GenBig.cpp大数据生成器:顶格 vs 随机
参数是「n 档位 C」。档位 0 是顶格形状(答案最大),档位 1 是照题面规模随机(答案几乎必然是 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.cpp尺子 + 两笔溢出账 + 秒表
参数是 N,默认满数据 200000。
// 三笔账 + 一把秒表
//
// 用法:./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 ③ 的 ansint WAN ≥ 92682 才现形)

★ 三个能过的版本差不到 4 倍,都在时限里 —— 所以这道题真正的门槛从来不是快慢, 是第 5、6 步那两个 WA

这一页记住三句话
  1. 能过的路有三条(二分 / 双指针 / 哈希)。 双指针不是「更正确」,只是把二分的 log 抹掉;考场上先写你最有把握的那一版。
  2. ★★★ 两个 bug,两种「对拍抓不到」: 重复元素那个,要平均每个值出现几次够大才抓得到(档位 1 密度 6.5 → 286/300, 而档位 0 两头都小、密度 0.6 → 只有 48/300,照题面规模随机则一次都抓不到); int 溢出那个,在小数据上算术不可能抓到 —— 线精确在 N = 92 682,只能算出来再去踩。
  3. ★★ 中间量也要算账。 aᵢ + C 最大 2 147 483 646int 上限 2 147 483 647 —— 富余量正好是 1。这题它躲过去了,但躲过去是算出来的,不是运气。