只允许相邻交换的排序,最小交换次数就是逆序对数;用归并排序在合并两个有序半段时顺便统计跨中线逆序对,O(n log n) 解决 n < 5×10^5 的规模。
OJ: roj
题目 ID: 3015
难度:提高
标签:逆序对归并排序树状数组
创建: 2026-10-01 10:13
更新: 2026-10-07 13:50
形式化题目
给定一个含
多个用例依次给出,读到
正解
思路
一句话本质:只交换相邻元素的排序,最少交换次数就等于序列的逆序对数——用归并排序在合并两个有序半段时顺便把逆序对数统计出来,复杂度
先看朴素做法:既然每次交换只动相邻两个元素,那就模拟冒泡,或者干脆双重循环枚举所有对
为什么答案是逆序对数? 逆序对指满足
- 对其他任何元素
,它与 、 的相对左右位置都没变,逆序状态不变; - 唯一变化的是这一对本身:从"逆序"变成"有序",逆序对数恰好减 1。
所以任何排序方案的交换次数
问题? 怎么在
分治。按位置把序列切成左右两半,逆序对
| 类型 | 怎么统计 | |
|---|---|---|
| 左内部 | 都在左半 | 递归统计 |
| 右内部 | 都在右半 | 递归统计 |
| 跨中线 | 合并两个有序半段时顺便统计 |
前两类交给递归。关键是第三类:先让左右两半各自有序,再归并合并。合并时两个指针分别扫左半
用样例序列
flowchart TD
A["原始: 9 1 0 5 4"] --> B["左半 [0,2): 9 1"]
A --> C["右半 [2,5): 0 5 4"]
B --> D["合并 9 与 1:<br/>1 取出 9 → +1<br/>有序: 1 9"]
C --> E["[5 4] 合并:<br/>4 取出 5 → +1<br/>再与 0 合并 → +0<br/>有序: 0 4 5"]
D --> F["顶层合并 [1 9] 与 [0 4 5]:<br/>0 取出 (1,9) → +2<br/>4 取出 9 → +1<br/>5 取出 9 → +1<br/>跨线共 +4"]
E --> F
F --> G["有序: 0 1 4 5 9<br/>合计 1 + 1 + 4 = 6 ✓"]
顶层合并的精算(左半有序
| 步骤 | 左半剩余 | 右半剩余 | 取出 | 跨中线计数 |
|---|---|---|---|---|
| 1 | 1 9 |
0 4 5 |
||
| 2 | 1 9 |
4 5 |
+0 | |
| 3 | 9 |
4 5 |
小于左半剩余 1 个 → +1 | |
| 4 | 9 |
5 |
小于左半剩余 1 个 → +1 | |
| 5 | 9 |
— | +0 |
顶层跨线共
实现细节:
- 递归函数
inversions(lo, hi)返回的逆序对数:区间长度 直接返回 0,否则 = 左递归 + 右递归 + merge_count(...)。 - 合并用预分配缓冲区
buf:先把a[lo:hi]整段拷进buf,再双指针写回a,保证归并后该段有序、不影响上层统计。 - 判断"取左半"的条件是
buf[i] <= buf[j]:本题元素互不相同,取不取等号不影响计数,但写<=让相等元素保持稳定,通用性更好。 - 答案上界是
,C++ 需要 long long;Python 的int天然无溢出。 - 多用例读到
为止:一次性读入全部 token,逐用例迭代解析。
另一条等价路线是树状数组 + 离散化:值域高达
,先把权值映射到排名,再从左往右插入,每个元素查询"已插入的数里比它大的个数"(或倒序查"比它小的个数"),复杂度同为 。本文选归并:不需要离散化,代码也更短。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 10:13
# update_at: 2026-10-01 10:13
import sys
from collections.abc import Iterator
def merge_count(a: list[int], buf: list[int], lo: int, mid: int, hi: int) -> int:
"""合并有序的 a[lo:mid] 与 a[mid:hi],返回跨中线的逆序对数,并就地排好。"""
buf[lo:hi] = a[lo:hi]
i, j, cross = lo, mid, 0
for k in range(lo, hi):
if j >= hi or (i < mid and buf[i] <= buf[j]):
a[k] = buf[i]
i += 1
else:
a[k] = buf[j]
j += 1
cross += mid - i # buf[j] 跳过的是右半还在等待的所有左半元素
return cross
def inversions(a: list[int], lo: int, hi: int, buf: list[int]) -> int:
"""统计 a[lo:hi] 的逆序对数:左内部 + 右内部 + 跨中线,归并排序一趟完成。"""
if hi - lo <= 1:
return 0
mid = (lo + hi) // 2
return inversions(a, lo, mid, buf) + inversions(a, mid, hi, buf) \
+ merge_count(a, buf, lo, mid, hi)
def answers(tokens: Iterator[int]) -> Iterator[str]:
"""依次处理每个用例,产出每个序列的最小交换次数。"""
for n in tokens:
if n == 0: # 终止用例
return
a = [next(tokens) for _ in range(n)]
yield str(inversions(a, 0, n, [0] * n))
def solve() -> None:
tokens = iter(map(int, sys.stdin.buffer.read().split()))
print('\n'.join(answers(tokens)))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 14:05
* update_at: 2026-10-06 14:05
*/
#include <cstdio>
typedef long long ll;
const int MAXN = 500005; // n < 5e5,数组开到 5e5+5 即可
ll a[MAXN]; // 当前用例的序列
ll buf[MAXN]; // 归并排序使用的缓冲区
// 合并有序的 a[lo..mid-1] 与 a[mid..hi-1],返回跨中线的逆序对数,并就地排好序。
ll merge_count(int lo, int mid, int hi) {
for (int i = lo; i < hi; i++) {
buf[i] = a[i];
}
int i = lo;
int j = mid;
ll cross = 0;
for (int k = lo; k < hi; k++) {
if (j >= hi || (i < mid && buf[i] <= buf[j])) {
a[k] = buf[i];
i++;
} else {
a[k] = buf[j];
j++;
cross += mid - i; // buf[j] 比左半还没被取走的所有元素都小
}
}
return cross;
}
// 统计 a[lo..hi-1] 的逆序对数:左半内部 + 右半内部 + 跨中线。
ll inversions(int lo, int hi) {
if (hi - lo <= 1) {
return 0;
}
int mid = (lo + hi) / 2;
ll left = inversions(lo, mid);
ll right = inversions(mid, hi);
ll cross = merge_count(lo, mid, hi);
return left + right + cross;
}
int main() {
int n;
while (scanf("%d", &n) == 1 && n != 0) {
for (int i = 0; i < n; i++) {
scanf("%lld", &a[i]);
}
printf("%lld\n", inversions(0, n));
}
return 0;
}复杂度
设单个用例长度为
- 时间:
。递归满足 ,正是归并排序的标准递推;合并中"一次减法统计一段"保证了合并本身是线性开销。 - 空间:
——原数组、等长缓冲区 buf,外加的递归栈。 - 所有元素互不相同,无需离散化;
时 Python 侧存在超时风险(本地实测含读入约 0.7s,接近 OJ 1s 限时),但算法与 C++ 正解同阶,同样的逻辑用 C++ 落地即稳过。
总结
- 相邻交换排序的最少交换次数 = 逆序对数:一次相邻交换恰消 1 个逆序对,升序终态逆序对数为 0,下界可达。
- 归并排序统计逆序对:逆序对按位置分三类,前两类递归,跨中线的一类在合并两个有序半段时由"右半取出一个元素 → 左半剩余全部比它大"一次性计
个。 - 复杂度
/ ,是逆序对问题的通用套路;等价替代是树状数组 + 离散化。 - 大答案要当心:
在 时超出 32 位整数范围。