Codeforces Round #301 (Div. 2) E. Infinite Inversions (分类讨论 逆序对)

题目链接

今天注定是不能补掉了,先把理解到的思路写一些。

一、题意

有一个无限长的序列{1, 2, 3, 4, ...}。现在给出n个操作,每个操作由a[i]和b[i]构成,表示第a[i]个数将和第b[i]个数交换位置。n不超过1e5,a[i]和b[i]不超过1e9。求操作后产生的逆序对总数。

二、思路

从官方题解(见E题部分)里面看了个大概,是将问题区分成两个部分。

可以知道,如果两个数都没有经历过操作,那么他们一定不会构成逆序对。于是构成逆序对的情况就可以分成这两种:1.构成逆序对的两个数都经过操作;2.构成逆序对的两个数一个经过操作、另一个没有经过,保留在原位置。

对于第一种情况,我们把操作中出现过的位置离散化之后,利用求逆序对的常规方法即可求出(归并或树状数组,然而我现在不会树状数组了,可能归并也不一定敲得出来了。归并这个感谢许轲出在了算法期末考试里,让我会了这个方法)。

对于第二种情况,明天再写。

好了,第二天了,开始写。第二种情况,我们假设i这个数字现在在to[i]的位置,那么和它构成第二种情况逆序对的数字只可能出现在( min(i, to[i]), max(i, to[i]) )的区间里。这个区间里数同样是两种:1.经历过操作的;2.没经历过操作的。我们需求的是第2种数的数量。如何得到呢?用区间里总共数的数量num1,减去,区间里经过操作过的数的数量num2。num1即abs(to[i] - i) - 1个数。num2如何求得?我们在前面求第一种情况的时候将经过操作的位置进行了离散化。所以我们可以用log(n)的时间找到离散化后to[i]和i位置,这两个位置相减后再减1,我们就得到了i和p[i]中间有多少已经经过操作的数了。num1 - num2就可以得到结果。对于每个数进行这样的运算,就得到了nlog(n)的算法。

理解第二种情况的处理方式,我是通过看了这个博客的代码。所以需要保存的都是经过操作的位置,保存它们的操作方式、操作后离散化以前的位置、操作后离散化以后的位置。

现在我需要复习一下逆序对求法、离散化。复习完了敲完了再贴代码。

三、代码

#include
using namespace std;
typedef long long ll;

const int max_n = 1e5 + 10;
pair op[max_n];
map mp;
//要注意最大出现数字的数量是两倍操作的数量
int aft[max_n * 2];
int orig[max_n * 2];
int n;
int seed = 0;
ll ans = 0;

int f[2 * max_n]; //树状数组

int lowbit(int x) {
	return x & (-x);
}

void add(int pos, int x) { //在第pos位置上 + x
	while(pos <= seed) {
		f[pos] += x;
		pos += lowbit(pos);
	}
}

int ask(int pos) { //pos位的前缀和
	// printf("ask[%d]\n", pos);
	int ans = 0;
	while(pos) {
		// printf("f[%d]:%d ", pos, f[pos]);
		ans += f[pos];
		pos -= lowbit(pos);
	}
	// printf("\n");
	return ans;
}


int main() {
	//输入
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		int a, b;
		scanf("%d %d", &a, &b);
		op[i] = make_pair(a, b);
		mp[a] = 0;
		mp[b] = 0;
	}
	//离散化到mp
	for (auto x = mp.begin(); x != mp.end(); x++) {
		x->second = ++seed;
		aft[seed] = seed;//用aft存离散化之后的顺序数
		orig[seed] = x->first;//用orig存离散化之前的数
	}
	//利用操作方式,在a中交换元素
	for (int i = 1; i <= n; i++) {
		int a = op[i].first, b = op[i].second;
		a = mp[a], b = mp[b];
		swap(aft[a], aft[b]);
	}

	// printf("aft:");
	// for (int i = 1; i <= seed; i++) {
	// 	printf("%d ", aft[i]);
	// }
	// printf("\n");

	//树状数组求第一种逆序对
	for (int i = seed; i > 0; i--) {
		ans += ask(aft[i] - 1);
		add(aft[i], 1);
		// for (int i = 1; i <= seed; i++) {
		// 	printf("f[%d]:%d ", i, f[i]);
		// }
		// printf("\n");
	}

	//利用交换后的a, abs(orig[aft[i]] - orig[i]) -1 - (abs(aft[i] - i) - 1) 求出第二种逆序对 
	for (int i = 1; i <= seed; i++) {
		ans += (abs(orig[aft[i]] - orig[i]) - abs(aft[i] - i));
	}
	cout << ans << endl;
	return 0;
}

四、总结

这道题学到挺多东西的。

首先是分类讨论。把这个特殊逆序对的问题通过离散化转化成普通逆序对解决一部分,再通过分析,利用变化后的位置解决另一部分。

然后就是所谓“普通逆序对”的问题了。晚上试图彻底学习树状数组,发现还是有困难的地方:为什么,i + lowbit(i)就是父节点的下标?又为什么i - lowbit(i)就是前一个父节点的下标?通过各种方式理解未果,只能暂且记住树状数组的用法结论。利用树状数组不断问询、更新的方式求逆序对是非常好的办法,从后往前扫待求的数组,问询树状数组中比当前这个值小的值的个数,比如当前值为a[i],我们求出a[i] - 1位的前缀和,就是在i位之后与a[i]构成逆序对的数字数量。然后把a[i]放到树状数组中,是将数组a[i]位进行add(1)。这就是所谓的“在数值范围内构建树状数组”。

2400分的题其实并不是不能做(感觉许轲就能做,哎,我菜爆了)。

你可能感兴趣的:(Codeforces Round #301 (Div. 2) E. Infinite Inversions (分类讨论 逆序对))