[Luogu 6186] NOI ONLINE 2020-#1-S T2 冒泡排序 题解
今天刷到这道题,在我写完一半之后机房大佬发现我在写这道题,他也来写。两人都写完后交流,发现做法不一样,他似乎是大众做法(动态维护需要开两个树状数组),然而我自己推的结论在维护的时候只要一个。
拿到题就开写,写了 50 行的线段树,然后发现白写了。其实只需要写两个树状数组,一个用于跑逆序对,另一个动态维护。
题意
动态维护一个长度为 n n n 的 排列 a a a,有两种操作,共 q q q 次:
- 修改:给定 x x x,交换 a x a_x ax 和 a x + 1 a_{x+1} ax+1。
- 查询:给定 x x x,求对这个排列冒泡排序 x x x 轮之后还剩下的逆序对数。
数据范围: 2 ≤ n , q ≤ 2 × 1 0 5 2 \le n, q \le 2 \times 10^5 2≤n,q≤2×105。
性质
对于 1 ≤ i ≤ n 1 \le i \le n 1≤i≤n,记 b i = ∑ j = 1 i − 1 [ a j > a i ] b_i = \sum_{j=1}^{i-1} [a_j > a_i] bi=∑j=1i−1[aj>ai](即以 i i i 结尾的逆序对个数)。显然 b b b 从小到大跑一遍树状数组可求。
对于 0 ≤ i < n 0 \le i < n 0≤i<n,记 c i = ∑ j = 1 n [ b j ≤ i ] c_i = \sum_{j=1}^{n} [b_j \le i] ci=∑j=1n[bj≤i]。
关键性质:进行 x x x 轮冒泡排序之后,会减少 n x − ∑ i = 0 x − 1 c i nx - \sum_{i=0}^{x-1} c_i nx−∑i=0x−1ci 组逆序对。
证明:
注意到,每一轮冒泡排序,会把 本轮 开始之前(注意不是排序开始前)
b
i
=
0
b_i = 0
bi=0 的数拎起来操作 (“拎”,是不是很生动形象?),它可能会跟右边交换
0
0
0 次、
1
1
1 次或者多次。
而 b i > 0 b_i > 0 bi>0 的数前面一定会有恰好一个大于它的数,跟它发生交换,它的逆序对个数就一定会恰好减少 1 1 1。
本轮结束之后,逆序对减少的个数就是 ∑ i = 1 n [ b i > 0 ] = n − ∑ i = 1 n [ b i = 0 ] \sum_{i=1}^n [b_i>0] = n - \sum_{i=1}^n[b_i=0] ∑i=1n[bi>0]=n−∑i=1n[bi=0]。
注意刚刚粗体的句子,特别地,原来恰好为 1 1 1 的现在会变成 0 0 0,在下一轮也会被拎起来,而原来 b i = 0 b_i = 0 bi=0 的下一轮仍然不变。
应用数学归纳法可知,第 x x x 轮会减少 n − ∑ i = 1 n [ b i ≤ x − 1 ] n - \sum_{i=1}^n[b_i \le x-1] n−∑i=1n[bi≤x−1] 组逆序对。
发现 ∑ i = 1 n [ b i ≤ x − 1 ] \sum_{i=1}^n[b_i \le x-1] ∑i=1n[bi≤x−1] 就是上面设的 c x − 1 c_{x-1} cx−1。
而前 x x x 轮一共减少 n x − ∑ i = 0 x − 1 c i nx - \sum_{i=0}^{x-1} c_i nx−∑i=0x−1ci 组。
动态维护
我们只需要用一个树状数组维护 c c c 的前缀和,同时维护排序开始之前的逆序对数。
注意到:每次 邻项交换 操作只会使得某个 b i b_i bi 加 1 1 1 或减 1 1 1。
如果 b i ← b i + 1 b_i \leftarrow b_i+1 bi←bi+1:
- 对于原来的 b i b_i bi 消失, ∀ b i ≤ i < n \forall b_i \le i < n ∀bi≤i<n, c i c_i ci 都应当减 1 1 1。
- 对于新的 b i + 1 b_i+1 bi+1 产生, ∀ b i + 1 ≤ i < n \forall b_i +1 \le i < n ∀bi+1≤i<n, c i c_i ci 都应当加 1 1 1。
- 发现 b i + 1 ≤ i < n b_i +1 \le i < n bi+1≤i<n 的部分,一加一减相互抵消,只需要 c b i ← c b i − 1 c_{b_i} \leftarrow c_{b_i} - 1 cbi←cbi−1 即可。
在没发现抵消之前,我以为要写线段树,事实上根本没必要。
如果 b i ← b i − 1 b_i \leftarrow b_i-1 bi←bi−1:
- 同理可知,后面的部分都是一加一减相抵消,只需要 c b i − 1 ← c b i − 1 + 1 c_{b_i-1} \leftarrow c_{b_i-1} + 1 cbi−1←cbi−1+1 即可。
因为 b b b 的值域是 [ 0 , n − 1 ] [0, n-1] [0,n−1],树状数组维护不了下标为 0 0 0 的情况,那么在树状数组上整体平移 1 1 1 即可。
代码
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
using LL = long long;
using LLL = __int128;
const int MAXN = 2e5+5;
const int mod = 1e9+7;
int read() {
char ch;
int res = 0, op = 0;
do ch = getchar(), op |= ch == '-'; while (ch<'0'||ch>'9');
do res = (res<<3)+(res<<1)+(ch&15), ch = getchar(); while (ch>='0'&&ch<='9');
return op ? -res : res;
}
int n, m;
int a[MAXN], b[MAXN], c[MAXN];
LL sum;
class BIT { // 两只 BIT,适合封装成类。一只的话装 namespace 就好了。
private:
LL f[MAXN];
public:
void update(int p, int v) {
for (; p <= n; p += p & -p) f[p] += v;
}
LL query(int p) {
LL res = 0;
for (; p; p -= p & -p) res += f[p];
return res;
}
} X, Y;
int main() {
#ifndef ONLINE_JUDGE
freopen("lg6186.in", "r", stdin);
freopen("lg6186.out", "w", stdout);
#endif
n = read(), m = read();
for (int i = 1; i <= n; ++i) a[i] = read();
for (int i = 1; i <= n; ++i) {
b[i] = X.query(n-a[i]);
++c[b[i]], sum += b[i];
X.update(n-a[i]+1, 1);
}
for (int i = 1; i < n; ++i) c[i] += c[i-1];
for (int i = 0; i < n; ++i) Y.update(i+1, c[i]);
while (m--) {
int op = read(), x = read();
x = min(x, n);
if (op == 1) {
if (a[x] < a[x+1]) Y.update(b[x]++ + 1, -1), ++sum;
else Y.update(--b[x+1] + 1, 1), --sum;
swap(a[x], a[x+1]);
swap(b[x], b[x+1]);
} else {
printf("%lld\n", sum - (1ll * n * x - Y.query(x)));
}
}
return 0;
}