把每个值左边更大元素的个数记为 $c[x]$,则做完 $k$ 轮冒泡后的逆序对数就是 $sum(max(c[x]-k,0))$,再用树状数组维护 $c[x]$ 的动态分布。
OJ: luogu
题目 ID: P6186
难度:提高+/省选-
标签:树状数组逆序对推导思维
日期: 2026-06-21 01:17
题意
给出一个
操作有两种:
- 交换当前位置
和 上的两个数 - 询问当前排列做完
轮标准冒泡排序后,还剩多少个逆序对
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:直接模拟相邻交换与冒泡排序轮数。
// 只适合小数据,用来帮助理解“每做一轮冒泡排序会发生什么”。
int count_inversion(const vector<int> &a) {
int n = (int) a.size();
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (a[i] > a[j]) {
ans++;
}
}
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
while (m--) {
int op;
cin >> op;
if (op == 1) {
int x;
cin >> x;
--x;
swap(a[x], a[x + 1]);
} else {
int k;
cin >> k;
vector<int> b = a;
int limit = min(k, n);
for (int round = 0; round < limit; round++) {
for (int i = 0; i + 1 < n; i++) {
if (b[i] > b[i + 1]) {
swap(b[i], b[i + 1]);
}
}
}
cout << count_inversion(b) << '\n';
}
}
return 0;
}brute.cpp 每次询问都直接复制当前排列,模拟
这个做法只能处理很小的数据。
真正关键的是先看懂“一轮冒泡”对逆序对的影响。
对每个值
c[x] = 当前在 x 左边、且比 x 大的数的个数
这就等于值
一轮冒泡时,1。
因此做完
总答案自然就是:
于是问题变成两件事:
- 当前每个
是多少 - 相邻交换后它们如何变化
相邻交换只会影响一对相邻数的大小关系:
- 如果交换的是顺序对
,那么 增加 1 - 如果交换的是逆序对
,那么 减少 1
也就是说,每次交换只改动一个
接着按
- 一棵统计每个
值出现了多少次 - 一棵统计这些
值的总和
查询
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
int n, m;
int p[MAXN]; // p[i] 表示当前位置上的数
int inv_cnt[MAXN]; // inv_cnt[x] 表示当前有多少个比 x 大的数在 x 左边
int bit_val[MAXN]; // 计算初始 inv_cnt 时使用:按值统计出现次数
int bit_num[MAXN]; // 按 inv_cnt 的取值统计有多少个数
long long bit_sum[MAXN]; // 按 inv_cnt 的取值统计 inv_cnt 总和
int lowbit(int x) {
return x & -x;
}
void add_bit(int bit[], int pos, int val) {
for (int i = pos; i <= n; i += lowbit(i)) {
bit[i] += val;
}
}
int sum_bit(int bit[], int pos) {
int ret = 0;
for (int i = pos; i > 0; i -= lowbit(i)) {
ret += bit[i];
}
return ret;
}
void add_bit_sum(int pos, long long val) {
for (int i = pos; i <= n; i += lowbit(i)) {
bit_sum[i] += val;
}
}
long long sum_bit_sum(int pos) {
long long ret = 0;
for (int i = pos; i > 0; i -= lowbit(i)) {
ret += bit_sum[i];
}
return ret;
}
// 在“inv_cnt 的分布”上删除旧值,再加入新值。
void modify_inv_cnt(int x, int new_value) {
int old_value = inv_cnt[x];
add_bit(bit_num, old_value + 1, -1);
add_bit_sum(old_value + 1, -old_value);
inv_cnt[x] = new_value;
add_bit(bit_num, new_value + 1, 1);
add_bit_sum(new_value + 1, new_value);
}
long long query_answer(int k) {
if (k >= n) {
return 0;
}
// 只统计 inv_cnt > k 的那些值。
int cnt = sum_bit(bit_num, n) - sum_bit(bit_num, k + 1);
long long s = sum_bit_sum(n) - sum_bit_sum(k + 1);
return s - 1LL * k * cnt;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> p[i];
}
// 预处理每个值 x 的 inv_cnt[x]:
// 扫描到 p[i] 时,前面一共有 i-1 个数,其中 <= p[i] 的个数能用树状数组求出,
// 因而“前面比它大的数”就是这两者之差。
for (int i = 1; i <= n; i++) {
int x = p[i];
int not_greater = sum_bit(bit_val, x);
inv_cnt[x] = (i - 1) - not_greater;
add_bit(bit_val, x, 1);
}
// 建立 inv_cnt 的值域分布。
for (int x = 1; x <= n; x++) {
add_bit(bit_num, inv_cnt[x] + 1, 1);
add_bit_sum(inv_cnt[x] + 1, inv_cnt[x]);
}
while (m--) {
int op;
cin >> op;
if (op == 1) {
int pos;
cin >> pos;
int a = p[pos];
int b = p[pos + 1];
if (a < b) {
// 顺序对交换后会变成逆序对,较小值 a 左边多了一个更大的数 b。
modify_inv_cnt(a, inv_cnt[a] + 1);
} else {
// 逆序对交换后会被消掉,较小值 b 左边少了一个更大的数 a。
modify_inv_cnt(b, inv_cnt[b] - 1);
}
swap(p[pos], p[pos + 1]);
} else {
int k;
cin >> k;
cout << query_answer(k) << '\n';
}
}
return 0;
}复杂度
- 初始预处理:
- 每次交换:
- 每次询问:
总复杂度:
空间复杂度:
总结
这题最难的不是数据结构,而是先把“冒泡若干轮后的逆序对”改写成:
- 每个值独立贡献
一旦得到这个式子,后面的在线交换维护就顺了。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
