acm-(贪心、异或、分治、思维)Codeforces Round #673 (Div. 2)E. XOR Inverse

10 篇文章 0 订阅
3 篇文章 0 订阅

题面
传送门
本题让求一个x,使得当数组a中的所有元素与它异或后形成的新数组b中的逆序对最少。
异或惯用套路是按位考虑,这里我们从高位开始考虑。当前位的所有元素无非只有0或1两种状态,异或1的话相当于01互换,也就是发生反转,考虑这样异或对数字有何影响,注意该位同为0的数字无论是否反转都不能改变它们的相对大小,该位同为1的数字也不能改变它们的相对大小。这个性质是解题的关键。
由于不太好描述,用图来举例。比如现在有5个数字 a [ 0 ] , a [ 1 ] , a [ 2 ] , a [ 3 ] , a [ 4 ] \mathbf{a[0],a[1],a[2],a[3],a[4]} a[0],a[1],a[2],a[3],a[4],每个数字写成二进制如下图所示图一
我们先考虑第一行,也就是所有数字的最高位。只看这一位的话逆序对显然有3对,顺序对则有1对,那么异或1后顺序对与逆序对反转,逆序对会变成1对,顺序对则有3对。那么异或1就可以减少逆序对。
方便起见编一下组,设 { a [ 1 ] } \mathbf{\{a[1]\}} {a[1]}为组1, { a [ 0 ] , a [ 2 ] , a [ 3 ] , a [ 4 ] } \mathbf{\{a[0],a[2],a[3],a[4]\}} {a[0],a[2],a[3],a[4]}为组二。
我们知道逆序对的总数量是等于组一与组二逆序对数+组一内部元素逆序对总数+组二内部逆序对总数,我们在这一位无论异或1还是0只能改变组一与组二逆序对数,并且不会影响组一内部元素逆序对总数+组二内部逆序对总数,而后面更低的位也无法改变组一与组二逆序对数,故在这一位只需要选择让逆序对减少的数即可,在这个例子中需要异或1。

第一行异或1后,现在考虑第二行。
图二
在上图中我们标注了一下组1和组2,必须要明确的是组1和组2已经互不相关了,因为无论怎么以后都是无法改变组1和组2之间的大小关系,现在只能改变组1和组2各自内部的数字间的逆序对数。
现在对于组1而言,逆序对和顺序对都为0,由于题目要求数字尽量小,我们异或0即可。
对于组2而言,逆序对有1对,顺序对有3对,显然异或0更优。
然后注意到组2内部会分成两个组(这两个组的大小关系在更低位无法被改变),组1内部只有一个数字,所以是1个组。考虑对组重新标号,那么现在就存在3个互不关联的组,可以表示为下图:
图三
然后我们发现当前位的三个组之间的大小关系都无法再被改变,只可能改变它们内部的数字的逆序对数目,于是我们再像前两步一样check每一组,看逆序对和顺序对分别是多少,然后加起来得到当前位的逆序对和顺序对数目,这也是能够反转的数目,最后在决定当前位是异或1还是异或0。
最后我们再重新分组,以此往复,直到check到最低位我们就得到了最终答案。

本题的难点主要在于代码不太好写,大致的代码思路就是从高到低check每一位,每次check都会算出当前位的顺序对和逆序对,然后决定该位是异或1还是0,最后重新分组。我们用数组 c o l [ i ] \mathbf{col[i]} col[i]代表元素 a [ i ] \mathbf{a[i]} a[i]属于第几组,那么每次分组的时候把同一组中,即 c o l \mathbf{col} col相同的元素分成两类,一类是当前位为1的,一类是当前位为0的即可,由于不太好描述,具体步骤参考代码注释。分完的新组在下一次check中会用到。
然后组内计算逆序对就比较简单,用 c n t [ i ] \mathbf{cnt[i]} cnt[i]统计第 i \mathbf{i} i组的1的数量,遍历数组元素的时候就边统计 c n t \mathbf{cnt} cnt边计算逆序对即可。

int n,a[maxn],cnt[maxn][2],tot,col[maxn],mark[maxn][2];//tot代表当前组数 
ll inv=0;//逆序对总数(题目要求输出的量) 
int solve(int cur){//check第cur位 
	int ct=0;ll ans1=0,ans2=0;//ans1是当前位逆序对数,ans2是当前位顺序对数 
	FOR(i,0,tot)cnt[i][0]=cnt[i][1]=0;
	FOR(i,0,n){//统计逆序对和顺序对 
		if(a[i]&(1<<cur)){
			cnt[col[i]][1]++;
			ans2+=cnt[col[i]][0];
		}else{
			cnt[col[i]][0]++;
			ans1+=cnt[col[i]][1];
		} 
	}
	FOR(i,0,tot){//mark[i][j]表示第i组中当前位为j的元素划分到的新组编号 
		mark[i][0]=mark[i][1]=-1;
	}
	tot=0;
	FOR(i,0,n){
		int o=(a[i]&(1<<cur))>0;//a[i]当前位为o 
		if(mark[col[i]][o]==-1){//如果第一次遇到第col[i]组中的当前位为o的元素 
			mark[col[i]][o]=tot++;//那么给一个新的编号 
			col[i]=mark[col[i]][o];//把编号给该元素 
		}else{//如果前面第col[i]组中当前位为o的元素已经出现过 
			col[i]=mark[col[i]][o];//那么把它的编号给当前元素即可 
		}
	}
	inv+=min(ans1,ans2);//贪心的选择最小的逆序对数 
	if(ans1<ans2){//逆序对小于顺序对  
		return 0;//当前位异或0更优 
	}else if(ans1>ans2){//逆序对大于顺序对 
		return 1<<cur;//当前位异或1更优 
	}else return 0;//如果两者相等,由于题目要求数字最小化,故选择0 
}
int main(){
	rd(&n);
	rd(a,n);
	tot=1;//初始有1组 
	int ans=0;
	ROF(i,30,0){//从第30位check到第0位 
		ans|=solve(i);
	}
	wrn(inv,ans);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值