题单 · 习题解析

洛谷 P1908 逆序对

★★★ 「照题面随机」精确漏掉了题面自己提醒的那件事(0/300);而「对拍查不出溢出」缺一个主语 —— 顶格能抓 84/100

原题:洛谷 P1908出自 第 11 章 分治 的题单题面本地存档:2026-08-27
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。

最近,TOM 老猫查阅到一个人类称之为「逆序对」的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 aᵢ > aⱼi < j 的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。

Update:数据已加强。

输入格式

第一行,一个数 n,表示序列中有 n 个数。

第二行 n 个数,表示给定的序列。序列中每个数字不超过 10⁹

输出格式

输出序列中逆序对的数目。

说明 / 提示

对于 25% 的数据,n ≤ 2500

对于 50% 的数据,n ≤ 4 × 10⁴

对于所有数据,1 ≤ n ≤ 5 × 10⁵

应该不会有人 O(n²) 过 50 万吧 —— 2018.8 chen_zhe。

输入输出样例

输入

6
5 4 2 6 3 1

输出

11

1这是本章的原题 —— 所以这一页要做的是另一件事

★ 算法在正文里,这一页量的是「你会怎么栽」

归并求逆序对怎么写,第 11 章正文从头讲到尾了,这里不重复。

这一页只做一件事:把这道题上三个真实的坑,各自量出一个数。

  • 第 ③ 步:<= 写成 < —— 照题面随机,300 轮抓到 0 次(而题面第三句就在提醒这件事);
  • 第 ④~⑥ 步:cnt 用 32 位 —— 正文说「对拍查不出这个错」, ★ 这一页把那句话卡到个位,并且给它加了个主语
  • 第 ⑦ 步:树状数组那条路 —— 它不只是「换个写法」, ★ 它是顶格对拍唯一能用的参照物
p1908.cpp★ 这一版就能 AC
// P1908 逆序对 —— 能 AC 的那一版:归并排序顺手数
//
// 和本章正文那份 fast.cpp 是同一个算法,差别只在**规模**:
// · 正文那份按 n = 10⁵ 写,用的是 vector<long long>;
// · 洛谷 P1908 的 n 上限是 **5×10⁵**,而且值域到 10⁹。
//
// 两处必须盯住的:
// ① cnt 必须是 long long —— n = 5×10⁵ 时最多 n(n-1)/2 ≈ **1.25×10¹¹** 对,
// int 的上限才 2.1×10⁹,差了 58 倍。(这条为什么对拍抓不到,见解析页第 ④ 步。)
// ② 读入 5×10⁵ 个数:关掉同步的 cin 就够(解析页第 ⑦ 步量过字节数)。
#include <bits/stdc++.h>
using namespace std;
static const int MAXN = 500005;
static int a[MAXN], tmp_[MAXN];
static long long cnt = 0;
void mergeSort(int l, int r) {
if (l >= r) return;
int mid = l + (r - l) / 2;
mergeSort(l, mid);
mergeSort(mid + 1, r);
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) { // ★ 必须是 <=:相等不算逆序对
tmp_[k++] = a[i++];
} else {
cnt += mid - i + 1; // ★ 左边剩下的全都比 a[j] 大
tmp_[k++] = a[j++];
}
}
while (i <= mid) tmp_[k++] = a[i++];
while (j <= r) tmp_[k++] = a[j++];
for (int p = l; p <= r; p++) a[p] = tmp_[p];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
for (int i = 0; i < n; i++) cin >> a[i];
if (n > 0) mergeSort(0, n - 1);
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

顶格数据(n = 5 × 10⁵,值域 10⁹ 随机)本机实测:0.05 秒,时限 1 秒。

2第 ① 版:暴力 —— 它对,只是跑不完

p1908Brute.cpp第 ① 版:O(n²)
// P1908 第 ① 版:每一对都看一遍,O(n²)
//
// 思路无可挑剔 —— 逆序对的定义是什么,它就照着数什么。
// 而且它**答案是对的**:下面对拍 1200 轮,它和归并版一次都没差过。
//
// 它唯一的毛病是慢,而这道题的题面自己把话说死了:
// 「应该不会有人 O(n²) 过 50 万吧 —— 2018.8 chen_zhe」
//
// n = 5×10⁵ ⇒ 1.25×10¹¹ 次比较。解析页第 ② 步有外推出来的秒数。
#include <bits/stdc++.h>
using namespace std;
static const int MAXN = 500005;
static int a[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
for (int i = 0; i < n; i++) cin >> a[i];
long long cnt = 0;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (a[i] > a[j]) cnt++;
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

思路无可挑剔:逆序对的定义是什么,它就照着数什么。而且它的答案是对的 —— 下面 1200 轮对拍,它和归并版一次都没差过。

本机实测(随机数据,n 每翻 2.5 倍耗时翻 6.25 倍,正好是平方):

n 暴力 O(n²) 归并 O(n log n)
20 000 0.44 秒
50 000 2.74 秒
500 000(顶格) ★ 外推 约 274 秒 0.05 秒

时限 1 秒。题面自己把话说死了:「应该不会有人 O(n²) 过 50 万吧」。

⚠ 但请先记住这一行 —— 它在第 ⑥ 步会变成主角

「暴力慢」在这里看着只是一句废话。可对拍的参照物就是它

⇒ 暴力有多慢,直接决定了你的对拍最大能开到多大的 n。 第 ⑥ 步那个死结就是从这一行长出来的。

3⚠ 错法一:少一个等号 —— 而「照题面随机」抓到 0 次

p1908Lt.cpp⚠ a[i] <= a[j] 写成 <
// P1908 ⚠ 错法二:`a[i] <= a[j]` 写成 `a[i] < a[j]`
//
// 相等的时候走了 else 分支 ⇒ 把「相等」也当成逆序对数了一遍,答案偏大。
//
// ★ 它和上面那个错法的性质完全不同:
// · cnt 用 int:对拍**结构上**抓不到(小数据根本不可能溢出);
// · 这一个:对拍抓得到,但**取决于数据里有没有重复值** ——
// 而这道题的题面明写「注意序列中可能有重复数字」,官方样例里却一个重复都没有。
// ⇒ 抓获率由**值域**说了算,和 n 关系不大。解析页第 ⑤ 步那张表是量出来的。
#include <bits/stdc++.h>
using namespace std;
static const int MAXN = 500005;
static int a[MAXN], tmp_[MAXN];
static long long cnt = 0;
void mergeSort(int l, int r) {
if (l >= r) return;
int mid = l + (r - l) / 2;
mergeSort(l, mid);
mergeSort(mid + 1, r);
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (a[i] < a[j]) { // ⚠ 就是这里少了一个等号
tmp_[k++] = a[i++];
} else {
cnt += mid - i + 1;
tmp_[k++] = a[j++];
}
}
while (i <= mid) tmp_[k++] = a[i++];
while (j <= r) tmp_[k++] = a[j++];
for (int p = l; p <= r; p++) a[p] = tmp_[p];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
for (int i = 0; i < n; i++) cin >> a[i];
if (n > 0) mergeSort(0, n - 1);
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

合并时 a[i] <= a[j] 写成 a[i] < a[j]:相等的那一对走了 else 分支, 被当成逆序对数了一遍,答案偏大。只有序列里有重复值时它才露馅。

样例是 5 4 2 6 3 1 —— 六个数互不相同。⇒ 它照过。

p1908Gen.cpp生成器:四档
// 数据生成器(P1908 对拍用):`./p1908Gen <seed> [level] [n]`
//
// level 0(默认)★ **照题面随机**:值域 1..10⁹ —— 几乎不可能撞出重复值
// level 1 ★ **值域 1..5** —— 同样的 n,重复值满地都是
// level 2 ★ **完全倒序的排列** n, n-1, …, 1(逆序对最多的形状,答案恰好 n(n-1)/2)
// level 3 完全升序(答案 0)+ n = 0/1 那几个边界,值域 1..5 所以还会带上相等
//
// 第三个参数可以**指定 n**(默认小数据)—— 解析页第 ④ 步就是拿
// `level 2, n = 65536` 和 `n = 65537` 这两条命令把溢出的那条线卡到个位的。
//
// ★ 这四档要回答的是**同一个问题的两半**:
// `<=` 写成 `<` 只在**有重复值**的时候露馅 ——
// 而「有没有重复值」由**值域**说了算,和 n 关系不大。
// ⇒ 照题面随机(level 0)抓不到它,不是因为轮数不够,是因为值域太宽。
//
// ⚠ 这四档**一个都抓不到 cnt 用 int 那个错** —— 那不是轮数问题,是算术问题:
// n ≤ 12 时逆序对最多 66 个,而 int 的上限是 21 亿。见 p1908Count.cpp。
#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) {
rng.seed((argc > 1) ? (unsigned)atoi(argv[1]) : 1);
int level = (argc > 2) ? atoi(argv[2]) : 0;
int n = (level == 3) ? ri(0, 3) : ri(1, 12);
if (argc > 3) n = atoi(argv[3]);
printf("%d\n", n);
vector<int> a(n);
for (int i = 0; i < n; i++) {
if (level == 2) a[i] = n - i; // ★ 严格倒序的排列,答案 = n(n-1)/2
else if (level == 1) a[i] = ri(1, 5);
else if (level == 3) a[i] = ri(1, 5);
else a[i] = ri(1, 1000000000);
}
if (level == 3) sort(a.begin(), a.end());
for (int i = 0; i < n; i++) printf("%d%c", a[i], i + 1 == n ? '\n' : ' ');
if (n == 0) printf("\n");
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

四档 × 300 轮,< 那一版被抓到的次数:

生成器 n 值域 被抓
level 0 ★ 照题面随机 1~12 1..10⁹ 0 / 300
level 1 1~12 1..5 251 / 300
level 2 完全倒序的排列 1~12 排列 0 / 300
level 3 升序 + n = 0..3 0~3 1..5 58 / 300
★★★ 看第一行和第二行:n 一个字都没改,只把值域从 10⁹ 换成 5

0 / 300 不是「运气不好」,它是算出来的n ≤ 12 时一共 66 对, 每对撞上的概率 1/10⁹ ⇒ 一轮里出现重复值的概率约 6.6 × 10⁻⁸, 300 轮的期望是 0.00002 次。这个 0 是结构性的。

抓获率的主语是值域,不是轮数,也不是 n —— 把值域压到 1..5,同样的 n、同样的 300 轮,抓获率直接跳到 84%。

★★★ 更值得记的是:这件事题面自己提醒过。 原文第三句写着「注意序列中可能有重复数字」—— 出题人特意写这一句,恰恰因为它是随机数据造不出来的那一档。

题面里的「注意 / 可能 / 不保证」是出题人给的情报, 而它指的往往正是「照题面随机」精确漏掉的那一档。

⚠ 顺带看 level 2 那一行:它也是 0,但原因完全不同

level 2 造的是完全倒序的排列 n, n-1, …, 1 —— 逆序对最多的形状, 听起来像是「最狠的数据」。可它是个排列,一个重复值都没有 ⇒ 对这个 bug 同样是精确的 0。

这就是第 7 章 P1638那条的再一次现场: 为一个 bug 精心造的档位,正好是另一个 bug 的盲区。 「最狠」要问清楚是对谁最狠。

4⚠ 错法二:cnt 用 32 位 —— 样例过,1200 轮对拍也全过

p1908Int.cpp⚠ cnt 只有 32 位
// P1908 ⚠ 错法一:cnt 用 32 位存 —— 全书最出名的 0 分写法
//
// 除了 cnt 的类型,和 p1908.cpp 一个字符都不差。
// 样例过、对拍 1200 轮全过、你在本地找不到任何理由怀疑它。
// 而它在洛谷上是 0 分。
//
// ⚠ 为什么这里写的是 `unsigned` 而不是真正的 `int`:
// 有符号整数溢出在 C++ 里是 **UB** —— 编译器可以假设它不发生,
// 于是 -O2 下这份代码的输出**不可复现**(换个编译器、换个优化等级就变)。
// `unsigned` 的回绕是标准规定的,转回 int 打印之后
// 得到的正是你在洛谷上看到的那个负数。⇒ 这样这一页的数字才钉得住。
//
// 那条线在哪儿:p1908Count.cpp 把它算到了个位(提示:n = 65537)。
#include <bits/stdc++.h>
using namespace std;
static const int MAXN = 500005;
static int a[MAXN], tmp_[MAXN];
static unsigned cnt = 0; // ⚠ 就是这里。正解是 long long
void mergeSort(int l, int r) {
if (l >= r) return;
int mid = l + (r - l) / 2;
mergeSort(l, mid);
mergeSort(mid + 1, r);
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) {
tmp_[k++] = a[i++];
} else {
cnt += (unsigned)(mid - i + 1);
tmp_[k++] = a[j++];
}
}
while (i <= mid) tmp_[k++] = a[i++];
while (j <= r) tmp_[k++] = a[j++];
for (int p = l; p <= r; p++) a[p] = tmp_[p];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
for (int i = 0; i < n; i++) cin >> a[i];
if (n > 0) mergeSort(0, n - 1);
cout << (int)cnt << "\n"; // 回绕之后再当成 int,就是洛谷上那个负数
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

除了 cnt 的类型,它和 p1908.cpp 一个字符都不差

  • 样例:11,过;
  • 上面那四档 × 300 轮 = 1200 轮对拍一次不一致都没有

你在本地找不到任何理由怀疑它。而它在洛谷上是 0 分

⚠ 这份演示代码里写的是 unsigned,不是 int

有符号整数溢出在 C++ 里是 UB-O2 下输出不可复现(换个编译器就变)。 unsigned 的回绕是标准规定的,转回 int 打印,得到的正是洛谷上那个负数。 ⇒ 这样这一页的数字才钉得住(第 45 章那条规矩)。

5★★★ 把那条线卡到个位

cnt 装不下,是从哪一个 n 开始的?这不用猜,它是一道算术题:

p1908Count.cpp把那条线算出来
// ★★★ 把「对拍为什么抓不到 int 溢出」算到个位
//
// 用法:./p1908Count 人话版
// ./p1908Count csv 给 check:viz 用
//
// 正文(第 11 章第 ② 步那个警告框)说的是「小数据的答案才几十,对拍会全过」。
// 那句话是对的,但它容易被读成「溢出这种错,对拍原理上看不见」——
// 而这份程序想说明的恰恰相反:**对拍看得见,只要你把 n 开到题面顶格。**
// 它抓不到的真正原因是一个**成本**问题,而成本能算:
//
// ① 触发线:完全倒序 / 随机排列,各要多大的 n 才可能越过 int 的上限;
// ② 三档生成器(n ≤ 12 / n ≤ 1000 / 顶格 n ≤ 5×10⁵)各能抓到几轮;
// ③ ★ 代价:**对拍的参照物是暴力**,而暴力在这些 n 上要跑多久。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
static const long long INT_MAX_LL = 2147483647LL;
static const int NMAX = 500000; // 题面顶格
static vector<int> buf;
static long long mergeCount(vector<int>& a, int l, int r) {
if (l >= r) return 0;
int mid = l + (r - l) / 2;
long long c = mergeCount(a, l, mid) + mergeCount(a, mid + 1, r);
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) buf[k++] = a[i++];
else { c += mid - i + 1; buf[k++] = a[j++]; }
}
while (i <= mid) buf[k++] = a[i++];
while (j <= r) buf[k++] = a[j++];
for (int p = l; p <= r; p++) a[p] = buf[p];
return c;
}
static long long invOf(vector<int> a) {
if (a.empty()) return 0;
buf.assign(a.size(), 0);
return mergeCount(a, 0, (int)a.size() - 1);
}
static vector<int> permOf(int n, mt19937& rng) { // 手写 Fisher–Yates,跨机器可复现
vector<int> a(n);
for (int i = 0; i < n; i++) a[i] = i + 1;
for (int i = n - 1; i > 0; i--) swap(a[i], a[rng() % (unsigned)(i + 1)]);
return a;
}
int main(int argc, char** argv) {
bool csv = (argc > 1 && string(argv[1]) == "csv");
/* ① 触发线 */
long long minNDesc = 0;
for (long long n = 2; ; n++) if (n * (n - 1) / 2 > INT_MAX_LL) { minNDesc = n; break; }
long long descInv = minNDesc * (minNDesc - 1) / 2;
long long descPrev = (minNDesc - 1) * (minNDesc - 2) / 2;
int descWrapped = (int)(unsigned)(unsigned long long)descInv;
int lo = 2, hi = 200000; // 随机排列:二分找第一个越界的 n(实测,不套公式)
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
mt19937 r(20260827u);
if (invOf(permOf(mid, r)) > INT_MAX_LL) hi = mid; else lo = mid + 1;
}
int minNRand = lo;
/* ② 三档生成器各抓几轮(100 轮;「抓到」= 真答案越过 int 上限) */
const int ROUNDS = 100;
const int CAPS[3] = {12, 1000, NMAX};
int caught[3] = {0, 0, 0};
long long bruteOps[3] = {0, 0, 0}; // 那 100 轮里暴力参照物要跑多少次比较
for (int t = 0; t < 3; t++) {
mt19937 rng(20260827u + (unsigned)t);
for (int r = 0; r < ROUNDS; r++) {
int n = (int)(rng() % (unsigned)CAPS[t]) + 1;
bruteOps[t] += (long long)n * (n - 1) / 2;
if (invOf(permOf(n, rng)) > INT_MAX_LL) caught[t]++;
}
}
/* ③ 暴力有多贵:在 minNDesc 上真跑一次,拿到「每秒多少次比较」再外推 */
int n = (int)minNDesc;
vector<int> desc(n);
for (int i = 0; i < n; i++) desc[i] = n - i;
auto t0 = steady_clock::now();
long long bcnt = 0;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (desc[i] > desc[j]) bcnt++;
double bruteMs = duration<double, milli>(steady_clock::now() - t0).count();
double opsPerMs = (double)((long long)n * (n - 1) / 2) / max(0.001, bruteMs);
double fullMin = (double)bruteOps[2] / opsPerMs / 60000.0; // 顶格那 100 轮要多少分钟
t0 = steady_clock::now();
long long fcnt = invOf(desc);
double fastMs = duration<double, milli>(steady_clock::now() - t0).count();
if (csv) {
printf("intMax,%lld\n", INT_MAX_LL);
printf("minNDesc,%lld\ndescInv,%lld\ndescPrev,%lld\ndescWrapped,%d\n",
minNDesc, descInv, descPrev, descWrapped);
printf("minNRand,%d\n", minNRand);
printf("rounds,%d\ncaught12,%d\ncaught1000,%d\ncaughtFull,%d\n",
ROUNDS, caught[0], caught[1], caught[2]);
printf("bruteOk,%d\nbruteSlower,%d\n", (bcnt == fcnt) ? 1 : 0, (bruteMs > fastMs * 50) ? 1 : 0);
printf("fullMinutes,%.0f\n", fullMin);
return 0;
}
printf("int 的上限:%lld\n\n", INT_MAX_LL);
printf("① 什么样的 n 才**可能**溢出\n");
printf(" 完全倒序:n = %lld ⇒ %lld(还在里面);n = %lld ⇒ %lld ★ 第一次越过,\n",
minNDesc - 1, descPrev, minNDesc, descInv);
printf(" 塞进 int 变成 %d\n", descWrapped);
printf(" 随机排列:最小的 n = %d(实测,不是套 n²/4 的公式)\n\n", minNRand);
printf("② 同一个 bug,三档生成器各抓到几轮(每档 %d 轮)\n", ROUNDS);
printf(" n ≤ %6d(顺手写的) %3d / %d\n", CAPS[0], caught[0], ROUNDS);
printf(" n ≤ %6d %3d / %d\n", CAPS[1], caught[1], ROUNDS);
printf(" n ≤ %6d(题面顶格) %3d / %d ★ 抓得到,而且很轻松\n\n", CAPS[2], caught[2], ROUNDS);
printf("③ ★ 那为什么大家还是抓不到?因为对拍的参照物是**暴力**\n");
printf(" 暴力在 n = %lld 上:%.0f 毫秒(%lld 次比较);归并只要 %.1f 毫秒\n",
minNDesc, bruteMs, (long long)n * (n - 1) / 2, fastMs);
printf(" ⇒ 顶格那 %d 轮,光暴力那一侧就要 **约 %.0f 分钟**(按上面那个速率外推)。\n\n", ROUNDS, fullMin);
printf(" ⇒ 结论不是「对拍看不见溢出」,是「顶格对拍的参照物不能是暴力」。\n");
printf(" 而这个 bug 有更便宜的抓法:n(n-1)/2 和 2³¹ 比一下 —— 一句算术。\n");
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

用生成器造两组只差一个数的完全倒序数据,喂给正解和 int 版:

n 逆序对真值 正解 int
65 536 2 147 450 880 2147450880 2147450880 ← 还活着
65 537 2 147 516 416 2147516416 -2147450880
★ 两条线,形状不同差了 42%
  • 完全倒序(逆序对最多):n(n-1)/2 > 2³¹-1 ⇒ 最小的 n65 537
  • 随机排列(期望是倒序的一半):实测二分,最小的 n92 742

⇒ 同一个 bug,换个数据形状,触发线就差 42%。 这是第 51 章「要造对形状」在溢出这类 bug 上的样子。

★ 而顺手写的对拍生成器 n ≤ 12,逆序对最多 66 个 —— 离 int 的上限差 32 537 631 倍这不是轮数问题,是算术问题第 6 章 P3406 那条)。

6★★★ 那对拍到底能不能抓到溢出?能。死结在别处

正文那个警告框写的是「对拍查不出溢出」。这一页把它验了一遍,结论是:

★★★ 那句话是对的,但它缺一个主语 ——「用小数据对拍」

把生成器的 n 开到题面顶格 n ≤ 5 × 10⁵,同一个 bug、同一套随机策略:

生成器的 n 100 轮抓到
n ≤ 12(顺手写的) 0 / 100
n ≤ 1000 0 / 100
n ≤ 5 × 10⁵(题面顶格) 84 / 100

溢出这个 bug,对拍抓得到,而且抓得很轻松。 它「抓不到」的全部原因是:没有人把对拍的 n 开到题面顶格。

而没有人这么干,也是有具体理由的 —— 见下面那一段。

★★★ 死结在「参照物是暴力」这五个字上

对拍的形状是「暴力 vs 你写的」。可回到第 ② 步那张表: 暴力在 n = 5 × 10⁵ 上要跑 约 274 秒

⇒ 顶格那 100 轮,光暴力那一侧就要约 17 分钟(按实测速率外推)。 ⇒ 于是所有人的对拍生成器都停在 n ≤ 20 —— 而那一档结构上不可能溢出。

这才是「对拍查不出溢出」的真正原因:不是原理上看不见, 是参照物自己先跑不动了

⇒ 想在顶格档对拍,参照物就必须换成另一个 O(n log n) 的正确实现(下一步那份树状数组)。 而那时候对拍验的已经不是「算法对不对」,是「两条互不相干的路会不会同时错」。

★ 但这个 bug 有个便宜一万倍的抓法

n(n-1)/22³¹ 比一下 —— 一句算术,三秒钟,而且是确定的

⇒ 这本书前面几十页都在教「造对数据去抓 bug」。这一页给出一个反例: 有一类 bug 根本不该用对拍去抓。 判据很简单:当 bug 的触发条件是「某个量超过某个常数」时,算比测快,而且算是充分的。

7★ 第 ② 条路:离散化 + 树状数组

p1908Bit.cpp★ 另一条路(第 38 章)
// P1908 ★ 另一条路:离散化 + 树状数组(第 38 章的写法,提前看一眼)
//
// 和归并完全无关的一条路 —— 而这正是它在这一页的价值:
// **两条互不相干的路算出同一个数**,比「同一条路跑两遍」有说服力得多。
//
// 思路:从右往左扫,每遇到一个 a[i],问「右边已经放进去的数里,有几个比它小」。
// 那就是以 i 为左端点的逆序对个数。树状数组按「值」建,单点加、前缀和查。
//
// ⚠ 值域到 10⁹,不能直接开数组 ⇒ **离散化**:排序去重后用「排名」当下标。
// ⚠ cnt 一样要 long long(换了算法不换这条线)。
#include <bits/stdc++.h>
using namespace std;
static const int MAXN = 500005;
static int a[MAXN], b[MAXN], tree_[MAXN];
static int m; // 去重之后有多少种值
static void add(int i) { for (; i <= m; i += i & -i) tree_[i]++; }
static int ask(int i) { int s = 0; for (; i > 0; i -= i & -i) s += tree_[i]; return s; }
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
for (int i = 0; i < n; i++) { cin >> a[i]; b[i] = a[i]; }
sort(b, b + n);
m = (int)(unique(b, b + n) - b);
for (int i = 0; i < n; i++)
a[i] = (int)(lower_bound(b, b + m, a[i]) - b) + 1; // 1..m 的排名
long long cnt = 0;
for (int i = n - 1; i >= 0; i--) {
cnt += ask(a[i] - 1); // 右边严格小于 a[i] 的个数(相等的不算)
add(a[i]);
}
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

从右往左扫,每遇到一个 a[i] 就问「右边已经放进去的数里,有几个比它小」。 值域到 10⁹ 开不了数组 ⇒ 先离散化1..m 的排名。 顶格数据实测 0.10 秒(归并那版 0.05 秒)。

★ 它在这一页有两个身份
  1. 验算走一条和归并完全无关的路 —— 两条互不相干的实现算出同一个数,比同一条路跑两遍有说服力得多 (第 7 章 P1147 那条)。
  2. ★★ 它是顶格对拍唯一能用的参照物(第 ⑥ 步那个死结的唯一出口)。

⚠ 注意 ask(a[i] - 1) 那个 -1:查的是严格小于。 写成 ask(a[i]) 就把「相等」也算进去了 —— 和第 ③ 步那个少掉的等号是同一个 bug, 只是换了一副身体。⇒ 它一样只在有重复值时露馅。

8一张总表

版本 做法 样例 1200 轮小数据对拍 顶格 结果
p1908Brute 每一对都看,O(n²) ✓ 全过 外推 274 秒 ✗ TLE
p1908Lt 归并,< 少个等号 ★ 照题面随机 0/300,值域压到 5 才 251/300 ✗ WA
p1908Int 归并,cnt 32 位 0/1200 ★ 顶格 84/100 0 分
p1908 归并 + long long 0.05 秒 AC
p1908Bit 离散化 + 树状数组 0.10 秒 AC
这一页记住三句话
  1. ★★★ 「照题面随机」会精确地漏掉题面自己特意提醒的那件事。 n 一个字没改、只把值域从 10⁹ 换成 5,抓获率就从 0/300 跳到 251/300。 而题面第三句写的正是「注意序列中可能有重复数字」—— 「注意 / 可能 / 不保证」是出题人给的情报。
  2. ★★★ 「对拍查不出溢出」缺一个主语:用小数据对拍查不出。 顶格 n ≤ 5 × 10⁵ 抓 84/100。死结不在原理上,在参照物是暴力上 —— 暴力在顶格档一轮 274 秒。⇒ 想顶格对拍,得先有第二条 O(n log n) 的路。
  3. ★★ 有一类 bug 根本不该用对拍抓。 触发条件是「某个量超过某个常数」时,n(n-1)/22³¹ 比一下 —— 算比测快,而且算是充分的。这条线在这道题上精确到个位:n = 65537