[Luogu 6186] NOI ONLINE 2020-#1-S T2 冒泡排序 题解

[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 2n,q2×105

性质

对于 1 ≤ i ≤ n 1 \le i \le n 1in,记 b i = ∑ j = 1 i − 1 [ a j > a i ] b_i = \sum_{j=1}^{i-1} [a_j > a_i] bi=j=1i1[aj>ai](即以 i i i 结尾的逆序对个数)。显然 b b b 从小到大跑一遍树状数组可求。

对于 0 ≤ i < n 0 \le i < n 0i<n,记 c i = ∑ j = 1 n [ b j ≤ i ] c_i = \sum_{j=1}^{n} [b_j \le i] ci=j=1n[bji]

关键性质:进行 x x x 轮冒泡排序之后,会减少 n x − ∑ i = 0 x − 1 c i nx - \sum_{i=0}^{x-1} c_i nxi=0x1ci 组逆序对。

证明

注意到,每一轮冒泡排序,会把 本轮 开始之前(注意不是排序开始前) 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]=ni=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] ni=1n[bix1] 组逆序对。

发现 ∑ i = 1 n [ b i ≤ x − 1 ] \sum_{i=1}^n[b_i \le x-1] i=1n[bix1] 就是上面设的 c x − 1 c_{x-1} cx1

而前 x x x 轮一共减少 n x − ∑ i = 0 x − 1 c i nx - \sum_{i=0}^{x-1} c_i nxi=0x1ci 组。

动态维护

我们只需要用一个树状数组维护 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 bibi+1

  • 对于原来的 b i b_i bi 消失, ∀ b i ≤ i < n \forall b_i \le i < n bii<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+1i<n c i c_i ci 都应当加 1 1 1
  • 发现 b i + 1 ≤ i < n b_i +1 \le i < n bi+1i<n 的部分,一加一减相互抵消,只需要 c b i ← c b i − 1 c_{b_i} \leftarrow c_{b_i} - 1 cbicbi1 即可。
  • 在没发现抵消之前,我以为要写线段树,事实上根本没必要。

如果 b i ← b i − 1 b_i \leftarrow b_i-1 bibi1

  • 同理可知,后面的部分都是一加一减相抵消,只需要 c b i − 1 ← c b i − 1 + 1 c_{b_i-1} \leftarrow c_{b_i-1} + 1 cbi1cbi1+1 即可。

因为 b b b 的值域是 [ 0 , n − 1 ] [0, n-1] [0,n1],树状数组维护不了下标为 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值