← 目录 第 56 / 60 节 全部 60 节

56 · 分治与 CDQ 思想

分治的三步骤说清后,先把归并排序改造成逆序对计数器,再把同一副骨架升到三维:排序管 a、归并管 b、树状数组管 c,得到 O(n log n log k) 的 CDQ 分治,顺带看清“每个答案对只在一个合并点被数一次”这条不重不漏的依据。
提高预计 70 分钟分治CDQ偏序
本节出现的新函数 / 新语法(先看懂再用)

上一节(回顾九)末尾给过一段十几行的归并排序求逆序对,说好“下一节再用到”,就是这里。那段代码看着像个小技巧,其实是分治的原型:把区间砍成两半,答案 = 左半 + 右半 + 跨中点的那一部分。前两项递归算,第三项在合并时顺手算完——分治的全部内容就是这个。

分治能成立只有一条理由:每个答案对恰好在一个合并点被数一次。想不清这一点,写出来必然重复计数或者漏算。
步骤归并排序逆序对(本节例题)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 被中点分开,它只可能在这一层被数到——更外层的合并只统计跨它自己中点的对,根本不会碰到它。这就是不重不漏的来源。

先看两块骨架:分治函数怎么写、合并的双指针怎么写。这两块立住了,三维偏序只是在合并里塞一棵树状数组。

新函数拆解 · 分治函数骨架 solve(l, r)
void solve(int l, int r) { if (l >= r) return; int mid = (l + r) >> 1; solve(l, mid); solve(mid + 1, r); /* 合并:只处理跨中点的贡献 */ }
组成含义 / 格式要求
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 次,常数直接翻倍。

新函数拆解 · 归并的双指针合并
int i = l, j = mid + 1, k = l; while (i <= mid && j <= r) tmp[k++] = (a[i] <= a[j]) ? a[i++] : a[j++];
组成含义 / 格式要求
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。

例题 · 逆序对洛谷 P1908普及/提高-去洛谷做这题 ↗

给定长度为 n 的正整数序列 a。若 i < j 且 aᵢ > aⱼ,则称 (i, j) 是一个逆序对,求逆序对总数。

输入格式:第一行一个整数 n;第二行 n 个整数,每个数不超过 10⁹。
输出格式:一行一个整数,表示逆序对的数目。

数据范围:1 ≤ n ≤ 5×10⁵。答案最大约 n(n−1)/2 ≈ 1.25×10¹¹,需用 64 位整数。

输入: 6 5 4 2 6 3 1 输出: 11
把“数对”拆成三份

暴力枚举每一对,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 的名次。代价是归并只能离线计数,改一个数就得重跑。

完整可编译代码(C++17):归并排序求逆序对
#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 里唯一的新语法:三元组结构体 + 去重。看上去是存储问题,其实是正确性问题。

新函数拆解 · 三元组结构体与去重(CDQ 的前置)
struct Node { int a, b, c, cnt, id; }; bool cmp(const Node &x, const Node &y);
组成含义 / 格式要求
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 的顺序改得面目全非。

例题 · 【模板】三维偏序(陌上花开)洛谷 P3810提高+/省选-去洛谷做这题 ↗

有 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⁷ 次操作。

完整可编译代码(C++17):CDQ 分治求三维偏序
#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 的样例,看输出怎么变,再想为什么。

数学补给 · 分治的 log n 是从哪一层数出来的

设一次规模为 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ⁱ本层总工作量
01nc·n
12n/2c·n
24n/4c·n
…………
log₂nn1c·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²),分治毫无收益。

举一反三 · 想清楚这些再往下走
  1. 若把逆序对的 “>” 换成 “>=”(相等也算),合并里哪个符号要改?答案会多出多少?
  2. 要求输出“每个位置 i 左边比它大的个数”,而不是总数,归并里的那一行要怎么改?要不要多开一个数组?
  3. 二维偏序(aᵢ ≤ aⱼ 且 bᵢ ≤ bⱼ、j 任意)为什么“排序 + 树状数组”就够,不需要分治?三维时缺的到底是哪一步?
  4. CDQ 里把树状数组换成“合并完再双指针扫一遍 b”,能省掉一个 log 吗?代价是什么?
  5. 带修改的逆序对(单点修改 + 查全局逆序对数)能不能用 CDQ?把什么当成第三维?
  6. n = 5×10⁵ 个点、但 k(属性最大值)只有 10 的偏序问题,O(n log n log k) 里的 log k 还有意义吗?该不该换算法?
自测:能全部回答再进下一节
挑战题 · 寒假作业
洛谷 P2717提高-去洛谷做这题 ↗

给定长度为 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 / 2C(n+1, 2) 可达 5×10⁹,先转 long long 再乘,否则中间就溢出成负数。
这题最容易错的地方
  • 下标只取 1..n,漏掉 S₀:所有“从第 1 个元素开始”的子段全丢 → WA,且答案恰好少若干个。
  • 取左边时漏掉等号:S_p == S_q 的对被当成逆序对减掉 → WA,测试数据里有“平均值恰好等于 k”的子段就会挂。
  • tot 用 int 算:5×10⁹ 溢出成负数 → WA,小样例照样过。
  • 把前缀和写成 S_x − k·x:等价于把 k 减了两次,样例立刻挂——这是这道题最常见的错。
  • 忘记把 tmp 抄回 s:上层合并看到乱序段,inv 偏小 → WA。
延伸阅读 / 参考