上一节(回顾九)末尾给过一段十几行的归并排序求逆序对,说好“下一节再用到”,就是这里。那段代码看着像个小技巧,其实是分治的原型:把区间砍成两半,答案 = 左半 + 右半 + 跨中点的那一部分。前两项递归算,第三项在合并时顺手算完——分治的全部内容就是这个。
| 步骤 | 归并排序 | 逆序对(本节例题) | CDQ 三维偏序 |
|---|---|---|---|
| ① 划分 | mid = (l + r) / 2 | 同左 | 先按 a 排序,再按位置切 |
| ② 递归 | solve(l, mid)、solve(mid+1, r) | 同左 | 同左 |
| ③ 合并 | 双指针按值归并 | 取右边时 ans += mid − i + 1 | 按 b 归并 + 树状数组查 c |
| 答案对在哪 | — | i 在左半、j 在右半 | 左半的 (b, c) 都不超过右半 |
| 复杂度 | O(n log n) | O(n log n) | O(n log n log k) |
动手前先回答三个问题,答不出就别写:① 怎么切(两半规模尽量相等);② 递归结束后两半各处于什么状态(归并排序:各自升序);③ 合并时怎么在 O(n) 或 O(n log n) 内把跨中点的贡献算完。第 ③ 问是分治的全部难点,前两问是套路。
为什么合并时只管跨中点的对:若 i、j 同在某半内部,这一对在递归到那一层时已经数过;若 i、j 被中点分开,它只可能在这一层被数到——更外层的合并只统计跨它自己中点的对,根本不会碰到它。这就是不重不漏的来源。
先看两块骨架:分治函数怎么写、合并的双指针怎么写。这两块立住了,三维偏序只是在合并里塞一棵树状数组。
| 组成 | 含义 / 格式要求 |
|---|---|
l, r | 当前处理的闭区间。用闭区间是为了让 mid 和 mid+1 直接对应“左段右端 / 右段左端”,不用额外 +1。 |
if (l >= r) return; | 出口。长度 ≤ 1 的区间天然有序,也没有跨中点的对。写成 l == r 在 l > r 时会漏掉保护。 |
int mid = (l + r) >> 1; | 取中点,>> 1 就是整除 2。l + r 超过 2.1×10⁹ 时才需要写成 l + (r - l) / 2,本题不需要。 |
两个递归调用 | 先递归、后合并。归并排序必须这个顺序:两半没排好,合并毫无意义。 |
合并段 | 整个分治唯一要动脑的地方。它只能看见“跨中点”的对,其余交给递归。 |
递归层数 | log₂n 层。n = 5×10⁵ 时 19 层,栈深度 19,绝不会爆栈。 |
#include <bits/stdc++.h>
using namespace std;
void solve(int l, int r) {
if (l >= r) return;
int mid = (l + r) >> 1;
cout << "[" << l << "," << r << "] mid = " << mid << "\n";
solve(l, mid); // 左半
solve(mid + 1, r); // 右半
}
int main() {
solve(1, 8); // 先看清“切”的形状,再往里加合并
return 0;
}跑一遍数层数:8 个元素切 3 层,第 0 层 1 段、第 1 层 2 段、第 2 层 4 段。
把 solve(1, 8) 改成 solve(1, 10),会看到 mid 向下取整,左段可能比右段长 1——所有分治都这样。
递归函数别传大对象(比如把整个 vector 按值传),每个元素会被复制 log n 次,常数直接翻倍。
| 组成 | 含义 / 格式要求 |
|---|---|
i | 左段指针,从 l 出发,最远走到 mid。 |
j | 右段指针,从 mid+1 出发,最远走到 r。 |
k | 写指针,从 l 出发。它写的是 tmp 而不是 a——原地写回会覆盖还没读的数据。 |
a[i] <= a[j] | 相等时优先取左边。这叫稳定,也是逆序对统计的正确性来源(相等不算逆序对)。 |
tmp[k++] = ... | 三件事压成一行:写入、k 前移、被选中那一侧的指针前移。 |
收尾两个 while | 一侧走空后,另一侧剩下的元素本就有序,直接照抄,不需要再比较。 |
最后的抄回 | 写完一段立刻抄回 a[l..r]。上层合并要求看到两半有序,这一步不能省。 |
#include <bits/stdc++.h>
using namespace std;
int a[9] = {0, 1, 4, 7, 2, 5, 8, 9, 9}; // a[1..3] 与 a[4..8] 各自升序
int tmp[9];
int main() {
int i = 1, j = 4, k = 1;
while (i <= 3 && j <= 8) tmp[k++] = (a[i] <= a[j]) ? a[i++] : a[j++];
while (i <= 3) tmp[k++] = a[i++]; // 左段剩余
while (j <= 8) tmp[k++] = a[j++]; // 右段剩余
for (int p = 1; p <= 8; ++p) cout << tmp[p] << " ";
cout << "\n"; // 1 2 4 5 7 8 9 9
return 0;
}示例把两段有序数据直接写死了,可以单独编译运行;真实代码里这三段(主循环 + 两个收尾)要放进 msort 的合并位置。
把 <= 改成 <,排序结果一模一样,但相等元素的先后被交换——逆序对统计会在这一点上出错,症状是 WA 而不是 RE。
给定长度为 n 的正整数序列 a。若 i < j 且 aᵢ > aⱼ,则称 (i, j) 是一个逆序对,求逆序对总数。
输入格式:第一行一个整数 n;第二行 n 个整数,每个数不超过 10⁹。
输出格式:一行一个整数,表示逆序对的数目。
数据范围:1 ≤ n ≤ 5×10⁵。答案最大约 n(n−1)/2 ≈ 1.25×10¹¹,需用 64 位整数。
暴力枚举每一对,n = 5×10⁵ 时是 1.25×10¹¹ 次比较,几十秒起步,必然 TLE。
拆成三份。设 solve(l, r) 给出区间内的逆序对数,则
solve(l, r) = solve(l, mid) + solve(mid+1, r) + cross(l, mid, r),cross 只数 i ≤ mid < j 且 a[i] > a[j] 的对。
让递归顺手排序。归并排序的递归本来就是“把两半排好”,于是 solve 除了返回答案,还要保证 a[l..r] 升序。这个附加状态正是 cross 能 O(n) 算出来的前提。
合并时怎么数。取右边的 a[j] 时,说明 a[i] > a[j];而 a[i..mid] 单调不降,所以这 mid − i + 1 个数全都 > a[j],一次贡献这么多对。取左边时 a[i] ≤ a[j],不构成逆序对,直接放走。
为什么不重复。每一对 (p, q) 在递归树上有唯一的最低公共层——它恰好横跨那一层的中点,于是只被那一层的合并数到一次。
等号的位置很关键。用 a[i] <= a[j] 判断取左边:相等时先放左边,相等的一对不进入答案。改一个符号就变成“非严格逆序对”,这是 WA 而不是 RE。
相对第 46 节的树状数组写法,这里省掉离散化。归并只比较大小,值到 10⁹ 也照样做;树状数组得先把值压成 1..n 的名次。代价是归并只能离线计数,改一个数就得重跑。
#include <bits/stdc++.h>
using namespace std;
const int N = 500005;
int n, a[N], tmp[N];
long long ans; // 上限 n(n-1)/2 ≈ 1.25e11,int 装不下
void msort(int l, int r) {
if (l >= r) return; // 长度 1,天然有序
int mid = (l + r) >> 1;
msort(l, mid); // 先让两半各自有序
msort(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 {
ans += mid - i + 1; // a[i..mid] 全都 > 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);
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
msort(1, n);
cout << ans << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
long long ans | 答案上限 1.25×10¹¹,用 int 会溢出成负数,且只在大测试点挂。 |
msort(1, n) | 对整段 1..n 调用,返回值放在全局 ans 里,省掉函数返回值的搬运。 |
if (l >= r) return; | 单元素区间既没有内部对,也没有跨中点的对,直接结束。 |
msort(l, mid); msort(mid + 1, r); | 两半排好序后,a[l..mid] 与 a[mid+1..r] 各自升序——这是合并能线性进行的前提。 |
ans += mid - i + 1 | 此刻 a[i] > a[j],而 a[i..mid] 升序,故这一段每个数都 > a[j],共 mid−i+1 个逆序对。 |
i++, j++ 的分工 | 只会有一个指针前移,所以整个 while 最多走 n 步,合并是 O(区间长度)。 |
for (p = l; p <= r; ++p) a[p] = tmp[p]; | 把合并结果写回原数组。漏掉这句,上层合并看到的是乱序,答案偏小。 |
tmp 是全局数组 | 不用每层新开,也避免递归里开大数组爆栈。每一层只写自己那一段 [l, r],互不干扰。 |
下面是正题。逆序对其实是二维偏序:数满足 i < j 且 a[i] > a[j] 的对,等价于“下标一维、值一维”。把下标换成属性 a、值换成属性 b,就是数 aᵢ ≤ aⱼ 且 bᵢ ≤ bⱼ 的对。再加第三维 c,就得到三维偏序:排序管一维,分治管一维,树状数组管一维。
| 维数 | 要数的东西 | 工具 | 复杂度 |
|---|---|---|---|
| 1 维 | aᵢ ≤ aⱼ 的对数 | 排序 + 双指针 | O(n log n) |
| 2 维(逆序对) | i < j 且 aᵢ > aⱼ | 归并 / 排序 + 树状数组 | O(n log n) |
| 3 维 | aᵢ ≤ aⱼ、bᵢ ≤ bⱼ、cᵢ ≤ cⱼ | 排序 + CDQ + 树状数组 | O(n log n log k) |
| 4 维以上 | 再叠一维 | CDQ 套 CDQ / 树套树 | O(n log³ n) 起步 |
先拆开 CDQ 里唯一的新语法:三元组结构体 + 去重。看上去是存储问题,其实是正确性问题。
| 组成 | 含义 / 格式要求 |
|---|---|
a, b, c | 三个属性。排序只按 a → b → c 的字典序,因为 CDQ 的“左半 ≤ 右半”依赖 a 单调。 |
cnt | 这个三元组出现了几次。完全相同的三元组必须合并成一个节点,否则它们在递归切分时的互相贡献会算错。 |
id | 合并后的编号,也是答案数组的下标。cdq 会反复打乱数组顺序,只有 id 能把答案找回去。 |
cmp 的三行 | 依次比 a、b、c;全相等时返回 false。这样 sort 之后,相同的三元组必然相邻。 |
去重循环 | 扫一遍:与上一组相同就 ++cnt,不同就开新组并记 id。因为已经排好序,一趟就够。 |
为什么必须去重 | 若两个元素三个属性都相同,它们互相满足 ≤,答案里各要算对方一次。合并成 cnt 后,用 cnt 参与树状数组计数,最后再补 cnt−1,就能一次算对。 |
#include <bits/stdc++.h>
using namespace std;
struct Node {
int a, b, c, cnt, id;
} q[10];
bool cmp(const Node &x, const Node &y) {
if (x.a != y.a) return x.a < y.a;
if (x.b != y.b) return x.b < y.b;
return x.c < y.c; // 全相等返回 false,保证相同元素相邻
}
int main() {
int raw[5][3] = {{1, 2, 3}, {1, 2, 3}, {2, 1, 1}, {1, 2, 3}, {2, 1, 1}};
int n = 5, tot = 0;
for (int i = 1; i <= n; ++i) {
q[i].a = raw[i - 1][0];
q[i].b = raw[i - 1][1];
q[i].c = raw[i - 1][2];
}
sort(q + 1, q + n + 1, cmp);
for (int i = 1; i <= n; ++i) {
if (tot && !cmp(q[tot], q[i])) ++q[tot].cnt; // 与上一组完全相同
else { q[++tot] = q[i]; q[tot].cnt = 1; q[tot].id = tot; }
}
for (int i = 1; i <= tot; ++i)
cout << q[i].a << " " << q[i].b << " " << q[i].c
<< " cnt=" << q[i].cnt << " id=" << q[i].id << "\n";
// 1 2 3 cnt=3 id=1 / 2 1 1 cnt=2 id=2
return 0;
}!cmp(q[tot], q[i]) 能判等,是因为数组已按 cmp 升序;此时“不小于”只能是“相等”。
去重后的元素个数 tot 可能远小于 n,后面的 cdq 只在 1..tot 上跑。
答案要按 id 存回,因为 cdq 的归并会把 q 的顺序改得面目全非。
有 n 个元素,第 i 个元素有三个属性 aᵢ、bᵢ、cᵢ。设 f(i) 为满足 aⱼ ≤ aᵢ 且 bⱼ ≤ bᵢ 且 cⱼ ≤ cᵢ 且 j ≠ i 的 j 的个数。
对每个 d ∈ [0, n),求 f(i) = d 的 i 的个数。
输入格式:第一行两个整数 n、k(k 是属性最大值);接下来 n 行,每行三个整数。
输出格式:共 n 行,第 d+1 行表示 f(i) = d 的 i 的个数。
数据范围:1 ≤ n ≤ 10⁵,1 ≤ aᵢ, bᵢ, cᵢ ≤ k ≤ 2×10⁵。
暴力是 O(n²) = 10¹⁰。二维可以用“排序 + 树状数组”降到 O(n log n),但三维时排完 a,剩下的 b、c 两维没法同时用一次扫描解决——这就是 CDQ 的用武之地。
第一维 a 交给排序。按 (a, b, c) 字典序排完,数组就满足 a 单调。于是任意“左半 / 右半”的切分都天然满足:左半每个元素的 a ≤ 右半每个元素的 a。
第二维 b 交给归并。合并时双指针按 b 归并:把左半元素按 b 递增塞进树状数组,遇到右半元素时,树状数组里的左半元素全都满足 b ≤ 当前元素的 b。
第三维 c 交给树状数组。进树状数组时在位置 c 上加 cnt,查询时算 sum(c),得到的就是“b、c 两维都不超过它”的左半元素个数,直接累加到答案。
答案对只在这个合并点被数一次。左半内部、右半内部的贡献在各自的递归里已经算完;跨中点的对恰好被这一层数到;更外层不会再碰它们。
去重是必须的。三个属性全相同的元素互相满足条件,但它们在排序后是同一组,不会再跨中点分开。所以合并成 cnt,进树状数组时加 cnt,最后再补上同组内另外 cnt−1 个。
每层结束要清空树状数组。第 l..mid 个元素被加过,必须原样减回去;否则外层的查询会把不属于它的元素也算进来,答案单调偏大。
复杂度。递归 log n 层,每层所有区间长度之和是 n,每个元素要么一次 add 要么一次 sum,各 O(log k) → O(n log n log k)。n = 10⁵、k = 2×10⁵ 时约 3×10⁷ 次操作。
#include <bits/stdc++.h>
using namespace std;
const int N = 100005; // 元素个数上限
const int M = 200005; // 属性值上限:树状数组下标是 c,要开到 2e5
int n, k, tot;
int bit[M]; // 树状数组,管第三维 c
int ans[N]; // ans[id]:比第 id 组小的元素个数
int res[N]; // res[d]:f(i) = d 的元素个数
struct Node {
int a, b, c, cnt, id;
} q[N], tmp[N];
bool cmp(const Node &x, const Node &y) {
if (x.a != y.a) return x.a < y.a;
if (x.b != y.b) return x.b < y.b;
return x.c < y.c;
}
int lowbit(int x) { return x & -x; }
void add(int i, int v) { for (; i <= k; i += lowbit(i)) bit[i] += v; }
int sum(int i) { int s = 0; for (; i > 0; i -= lowbit(i)) s += bit[i]; return s; }
void cdq(int l, int r) {
if (l >= r) return;
int mid = (l + r) >> 1;
cdq(l, mid); // 先把两半内部的答案算完
cdq(mid + 1, r); // 两个递归结束后,两半各自按 b 有序
// 合并:按 b 归并,只统计“左半 → 右半”的贡献
int i = l, j = mid + 1, t = l;
while (i <= mid && j <= r) {
if (q[i].b <= q[j].b) { // 左半这个 b 更小,它可以贡献给后面所有右半元素
add(q[i].c, q[i].cnt); // 第三维交给树状数组
tmp[t++] = q[i++];
} else {
ans[q[j].id] += sum(q[j].c); // 树里的都满足 b 更小,再查 c <= 它的有几个
tmp[t++] = q[j++];
}
}
while (i <= mid) { add(q[i].c, q[i].cnt); tmp[t++] = q[i++]; }
while (j <= r) { ans[q[j].id] += sum(q[j].c); tmp[t++] = q[j++]; }
for (int p = l; p <= mid; ++p) add(q[p].c, -q[p].cnt); // 撤销本层,别污染外层
for (int p = l; p <= r; ++p) q[p] = tmp[p]; // 按 b 有序写回
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
for (int i = 1; i <= n; ++i) cin >> q[i].a >> q[i].b >> q[i].c;
sort(q + 1, q + n + 1, cmp);
for (int i = 1; i <= n; ++i) { // 相同三元组合并成一组
if (tot && !cmp(q[tot], q[i])) ++q[tot].cnt;
else { q[++tot] = q[i]; q[tot].cnt = 1; q[tot].id = tot; }
}
cdq(1, tot);
for (int i = 1; i <= tot; ++i) // 同组内另外 cnt-1 个也要算
res[ans[q[i].id] + q[i].cnt - 1] += q[i].cnt;
for (int d = 0; d < n; ++d) cout << res[d] << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
int bit[M] | 树状数组下标是第三维 c,最大 2×10⁵。只开到 N = 10⁵ 会越界写,症状是 RE 或随机 WA。 |
sort(q + 1, q + n + 1, cmp) | 先把 a 排成单调。之后任何“左半 / 右半”切分都满足左半 a ≤ 右半 a。 |
!cmp(q[tot], q[i]) | 数组已升序,cmp 返回 false 只可能是相等。于是相同三元组被并成一组,cnt 记次数。 |
cdq(l, mid); cdq(mid + 1, r); | 顺序不能反:合并要求两半各自按 b 有序,而这个有序正是递归产出的。 |
if (q[i].b <= q[j].b) | 谁 b 小谁先动。左半元素进树的条件就是它的 b ≤ 当前右半元素的 b。 |
add(q[i].c, q[i].cnt) | 按 cnt 加,因为这一组背后有 cnt 个真实元素,每个都要各贡献 1。 |
ans[q[j].id] += sum(q[j].c) | 树里全是 b 不超的左半元素,sum(c) 数出其中 c 也不超的个数,正是这一对的贡献。 |
add(q[p].c, -q[p].cnt) 的撤销 | 把本层的加法原样减回去。漏了这句,外层查询会混入不属于它的元素,答案单调偏大。 |
q[p] = tmp[p] | 按 b 有序写回,供上一层继续二路归并。不写回,上一层看到的是乱序,答案偏小。 |
res[ans[...] + cnt - 1] += cnt | 同组内 cnt 个元素互相都满足 ≤,所以每个的 f 值是 ans + cnt − 1;这一组共 cnt 个元素,一起加。 |
这一节的骨架可以套在三个方向上,都是同一句话:把“枚举所有对”换成“在每个合并点批量处理跨中点的对”。
CDQ 优化 DP 的形状:dp[i] 只从 j < i 且满足某个偏序条件的 j 转移。把“枚举 j”换成分治:算完左半的 dp,就用左半的所有 dp 值去批量更新右半的候选,O(n²) 直接掉到 O(n log² n)。判断能不能用的标准只有一个——转移能不能写成跨中点的偏序计数。
不能用的信号:合并时无法在 O(n) 或 O(n log n) 内算清跨中点的贡献(比如要求跨中点区间的某种最值搭配),那就得换工具,硬套分治只会得到一个更慢的暴力。
把 a[i] <= a[j] 写成 a[i] < a[j]:相等的一对被算成逆序对 → WA,而答案恰好偏大“相同元素对数”那么多;没有重复数的样例完全测不出来。
ans 用 int:上限 1.25×10¹¹,溢出成负数 → WA,小数据全对,大数据全挂。
忘记 a[p] = tmp[p] 抄回:上层合并看到的是乱序段 → WA,答案偏小,且错得很隐蔽。
CDQ 里忘记撤销树状数组(add(q[p].c, -q[p].cnt)):外层查询混入不属于它的元素 → WA,答案单调偏大,n 越大错得越离谱。
CDQ 没做相同三元组的合并:三个属性全相同的元素在递归里永远同侧,互相的贡献全丢 → WA;只要数据有重复就挂,没重复就全对。
树状数组只开到 N = 10⁵ 而属性 c 能到 2×10⁵:越界写 → RE,或者更糟——写坏别的全局变量。
关掉页面,把 P1908 的 30 行从零敲一遍,提交确认样例输出 11。
改一改①:把 a[i] <= a[j] 改成 a[i] < a[j]。先用样例跑(答案仍是 11,因为没有重复数),再输入 3 换行 3 3 1,预测并验证:正确答案 2,改后变成 3。
改一改②:在合并前打印 l r mid ans,观察 ans 只在“取右边”时增长——这就是 cross 的定义在代码里的样子。
改一改③:把 CDQ 里撤销树状数组那一行注释掉,用 n = 10 的小数据跑 P3810 的样例,看输出怎么变,再想为什么。
设一次规模为 n 的归并耗时不超过 c·n(每个元素搬进 tmp 再抄回,常数次操作)。三步骤直接给出递归式
T(n) = 2T(n/2) + c·n,T(1) = O(1)。
按层展开,每层的工作量居然完全相同:第 i 层有 2ⁱ 段,每段长 n/2ⁱ,一段的合并代价是 c·n/2ⁱ,相乘还是 c·n。层数是 log₂n + 1。
| 层 i | 区间段数 2ⁱ | 每段长度 n / 2ⁱ | 本层总工作量 |
|---|---|---|---|
| 0 | 1 | n | c·n |
| 1 | 2 | n/2 | c·n |
| 2 | 4 | n/4 | c·n |
| … | … | … | … |
| log₂n | n | 1 | c·n |
所以 T(n) = c·n·(log₂n + 1) = O(n log n)。n = 5×10⁵ 时 log₂n ≈ 18.9,总操作约 5×10⁵ × 19 ≈ 9.5×10⁶——比 O(n²) 的 1.25×10¹¹ 小四个数量级。
CDQ 多出来的那个 log 来自树状数组。每层所有区间长度之和恰好是 n,每个元素在每层要么 add 一次、要么 sum 一次,各 O(log k)。于是每层 O(n log k),共 log n 层:O(n log n log k)。n = 10⁵、k = 2×10⁵ 时约 10⁵ × 17 × 18 ≈ 3×10⁷。
先估答案位数再写类型。逆序对最多 n(n−1)/2,n = 5×10⁵ 时是 124999750000 ≈ 1.25×10¹¹,12 位十进制。int 上限 2.147×10⁹,差两个数量级,所以 long long 不是保险,是必须。
主定理一句话:T(n) = aT(n/b) + f(n),本题 a = b = 2、f(n) = Θ(n),结果 Θ(n log n)。注意结论的另一半——合并做多慢,分治就多慢:若合并要 Θ(n²),总复杂度就是 Θ(n²),分治毫无收益。
ans += mid - i + 1 为什么可以一次加一整段,且不会重复。给定长度为 n 的正整数序列 a,求有多少个连续子序列的平均值不小于给定的 k。
输入格式:第一行两个整数 n、k;接下来 n 行,每行一个整数,第 i 个数是 aᵢ。
输出格式:一行一个整数,表示答案。
数据范围:1 ≤ n ≤ 10⁵,1 ≤ aᵢ, k ≤ 10⁴。答案最大约 n(n+1)/2 ≈ 5×10⁹,需用 64 位整数。
平均值的除法不好处理:它把整数比较变成了小数比较,也破坏了“能和前缀和相减”的性质。
试着把 k 从每个数里先减掉。子段 [i, j] 的平均值 ≥ k,等价于这段 (a − k) 的和 ≥ 0。
对 (a − k) 做前缀和 S[0..n]。子段 [i, j] 对应一对下标 (i−1, j),且 i−1 < j。问题变成数“有多少对 S[p] ≤ S[q]”。
变形。sum(a[i..j]) ≥ k·(j−i+1) 等价于 sum(a[i..j] − k) ≥ 0。令 bᵢ = aᵢ − k,S₀ = 0、S_x = b₁+…+b_x,则子段 [i, j] 合法 ⟺ S_j ≥ S_{i−1}。
数对。于是答案是:数下标对 (p, q),0 ≤ p < q ≤ n,满足 S_p ≤ S_q。注意这里共有 n+1 个前缀和,S₀ 必须参与。
反过来数。不满足的就是 S_p > S_q 的对,即严格逆序对数 inv。总对数 C(n+1, 2),答案 = 总对数 − inv。这样可以直接套本节模板,不必在合并里额外处理“等于”。
求 inv。把这 n+1 个 long long 前缀和交给归并排序,用 a[i] <= a[j] 取左边,答案 O(n log n)。数值规模:|aᵢ − k| ≤ 10⁴,n·10⁴ = 10⁹,前缀和不会超 int,但比较与相减统一用 long long 更省心。
为什么不能把 k 直接乘进下标。常见的错法是把 S_x 写成 S_x − k·x:那等于把 k 减了两次,样例就会挂。
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, k;
long long s[N], tmp[N], inv; // s: 减掉 k 之后的前缀和
void msort(int l, int r) {
if (l >= r) return;
int mid = (l + r) >> 1;
msort(l, mid);
msort(mid + 1, r);
int i = l, j = mid + 1, t = l;
while (i <= mid && j <= r) {
if (s[i] <= s[j]) tmp[t++] = s[i++]; // 相等不算逆序对
else { inv += mid - i + 1; tmp[t++] = s[j++]; }
}
while (i <= mid) tmp[t++] = s[i++];
while (j <= r) tmp[t++] = s[j++];
for (int p = l; p <= r; ++p) s[p] = tmp[p];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
s[0] = 0; // 空前缀,别忘了它
for (int i = 1; i <= n; ++i) {
long long a;
cin >> a;
s[i] = s[i - 1] + (a - k); // 把“平均值 ≥ k”化成“前缀和不降”
}
msort(0, n); // n+1 个数,下标 0..n
long long tot = (long long)(n + 1) * n / 2;
cout << tot - inv << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
s[i] = s[i - 1] + (a - k) | 把 k 摊到每个元素上:sum(a) − k·len ≥ 0 等价于 sum(a − k) ≥ 0。一次前缀和就把区间条件变成两点比较。 |
msort(0, n) | 0..n 共 n+1 个前缀和。写成 msort(1, n) 会漏掉所有“从第 1 个元素开始”的子段。 |
s[i] <= s[j] | 取等号:S_p == S_q 表示子段和为 0,平均值恰好等于 k,是合法答案,不能算成逆序对。 |
inv += mid - i + 1 | 左半剩下的 i..mid 都严格大于 s[j],一次加一整段,这是 O(n log n) 的关键。 |
tot - inv | 总对数减去“S_p > S_q”的对数,剩下的正是“S_p ≤ S_q”。 |
(long long)(n + 1) * n / 2 | C(n+1, 2) 可达 5×10⁹,先转 long long 再乘,否则中间就溢出成负数。 |