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 逆序对 —— 能 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;}点「运行 ▶」看结果
顶格数据(n = 5 × 10⁵,值域 10⁹ 随机)本机实测:0.05 秒,时限 1 秒。
2第 ① 版:暴力 —— 它对,只是跑不完
// 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;}点「运行 ▶」看结果
思路无可挑剔:逆序对的定义是什么,它就照着数什么。而且它的答案是对的 —— 下面 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 次
// 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;}点「运行 ▶」看结果
合并时 a[i] <= a[j] 写成 a[i] < a[j]:相等的那一对走了 else 分支,
被当成逆序对数了一遍,答案偏大。只有序列里有重复值时它才露馅。
样例是 5 4 2 6 3 1 —— 六个数互不相同。⇒ 它照过。
// 数据生成器(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 |
0 / 300 不是「运气不好」,它是算出来的:n ≤ 12 时一共 66 对,
每对撞上的概率 1/10⁹ ⇒ 一轮里出现重复值的概率约 6.6 × 10⁻⁸,
300 轮的期望是 0.00002 次。这个 0 是结构性的。
而抓获率的主语是值域,不是轮数,也不是 n ——
把值域压到 1..5,同样的 n、同样的 300 轮,抓获率直接跳到 84%。
★★★ 更值得记的是:这件事题面自己提醒过。 原文第三句写着「注意序列中可能有重复数字」—— 出题人特意写这一句,恰恰因为它是随机数据造不出来的那一档。
⇒ 题面里的「注意 / 可能 / 不保证」是出题人给的情报, 而它指的往往正是「照题面随机」精确漏掉的那一档。
level 2 造的是完全倒序的排列 n, n-1, …, 1 —— 逆序对最多的形状,
听起来像是「最狠的数据」。可它是个排列,一个重复值都没有 ⇒ 对这个 bug 同样是精确的 0。
这就是第 7 章 P1638那条的再一次现场: 为一个 bug 精心造的档位,正好是另一个 bug 的盲区。 「最狠」要问清楚是对谁最狠。
4⚠ 错法二:cnt 用 32 位 —— 样例过,1200 轮对拍也全过
// 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;}点「运行 ▶」看结果
除了 cnt 的类型,它和 p1908.cpp 一个字符都不差。
- 样例:
11,过; - 上面那四档 × 300 轮 = 1200 轮对拍:一次不一致都没有。
你在本地找不到任何理由怀疑它。而它在洛谷上是 0 分。
有符号整数溢出在 C++ 里是 UB,-O2 下输出不可复现(换个编译器就变)。
unsigned 的回绕是标准规定的,转回 int 打印,得到的正是洛谷上那个负数。
⇒ 这样这一页的数字才钉得住(第 45 章那条规矩)。
5★★★ 把那条线卡到个位
cnt 装不下,是从哪一个 n 开始的?这不用猜,它是一道算术题:
// ★★★ 把「对拍为什么抓不到 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 |
- 完全倒序(逆序对最多):
n(n-1)/2 > 2³¹-1⇒ 最小的n是 65 537; - 随机排列(期望是倒序的一半):实测二分,最小的
n是 92 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) 的正确实现(下一步那份树状数组)。
而那时候对拍验的已经不是「算法对不对」,是「两条互不相干的路会不会同时错」。
n(n-1)/2 和 2³¹ 比一下 —— 一句算术,三秒钟,而且是确定的。
⇒ 这本书前面几十页都在教「造对数据去抓 bug」。这一页给出一个反例: 有一类 bug 根本不该用对拍去抓。 判据很简单:当 bug 的触发条件是「某个量超过某个常数」时,算比测快,而且算是充分的。
7★ 第 ② 条路:离散化 + 树状数组
// 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;}点「运行 ▶」看结果
从右往左扫,每遇到一个 a[i] 就问「右边已经放进去的数里,有几个比它小」。
值域到 10⁹ 开不了数组 ⇒ 先离散化成 1..m 的排名。
顶格数据实测 0.10 秒(归并那版 0.05 秒)。
- 验算走一条和归并完全无关的路 —— 两条互不相干的实现算出同一个数,比同一条路跑两遍有说服力得多 (第 7 章 P1147 那条)。
- ★★ 它是顶格对拍唯一能用的参照物(第 ⑥ 步那个死结的唯一出口)。
⚠ 注意 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 |
- ★★★ 「照题面随机」会精确地漏掉题面自己特意提醒的那件事。
n一个字没改、只把值域从10⁹换成5,抓获率就从0/300跳到251/300。 而题面第三句写的正是「注意序列中可能有重复数字」—— 「注意 / 可能 / 不保证」是出题人给的情报。 - ★★★ 「对拍查不出溢出」缺一个主语:用小数据对拍查不出。
顶格
n ≤ 5 × 10⁵抓 84/100。死结不在原理上,在参照物是暴力上 —— 暴力在顶格档一轮 274 秒。⇒ 想顶格对拍,得先有第二条O(n log n)的路。 - ★★ 有一类 bug 根本不该用对拍抓。
触发条件是「某个量超过某个常数」时,
n(n-1)/2和2³¹比一下 —— 算比测快,而且算是充分的。这条线在这道题上精确到个位:n = 65537。