[十二省联考2019]异或粽子

传送门

题解

看到区间的xor,有一个常见的套路是求一次前缀xor和,这样一个区间的xor就可以表示为两个前缀的xor了。
于是问题转化为:给定n+1个数(注意最开头的长度为0的前缀也要算),求两两xor的前k大。
一道经典的问题是求两两xor的最大值是多少,相信大家应该都会这个trie树上贪心的做法:从左往右扫过去,每次看一下这个数与之前的数的xor的最大值是多少,只需要在trie树上贪心地尽可能往相反的方向走即可,在check完这个数之后再把它插入trie。

而这个题要求的是前k大的值,该怎么办呢?

考场上我看到这个前k大,首先想到的是另一道经典题:给定2个数组,求两两之和的前k小值(P1631序列合并)。

这个题很简单:只需要排序后用一个堆来维护,初始时堆中有a数组的每一个数和b数组的最小值的和,每次弹出堆顶时,将a数组对应的数和b数组的下一个数之和插入堆中即可。

同样地,这个题我们也可以用堆来维护:不妨先对于每个右端点,找到与它xor最大的左端点的位置,每次从堆中弹出一个元素时,固定右端点不变,寻找下一个左端点位置即可。

因为我们需要令左端点位置时刻小于右端点位置,就需要对于每个前缀都建一棵trie树,用可持久化trie来维护即可。

每次弹出堆顶时,需要的操作相当于要在trie树上找到与当前值xor第x大的元素,这个操作可以用记录trie树上的每个点的size来实现(类似于平衡树的查找第k大元素吧)。我在考场上写的是简单粗暴地从当前元素开始在trie树上向上跳,直到找到一个分叉之后走进去继续贪心。

时空复杂度都是1个log。

总结:题目难度并不大,作为省选d1t1的温暖题还是很合适的。

#pragma GCC optimize(2)
#include <cstdio>
#include <iostream>
#include <queue>
using namespace std;
typedef long long LL;
LL read()
{
	LL f=1,x=0;char s=getchar();        
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}       
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+(s^48);s=getchar();}     
    return x*f; 
}
const int maxn=500005;
const int maxs=16000005;
struct zz
{
	int id,k;
	LL val;
	friend bool operator <(zz a,zz b)
	{
		return a.val<b.val;
	}
};
priority_queue<zz> Q;
int n,m,ncnt,node[maxs][2],cnt[maxs];
LL s[maxn];
void addEage(int x)
{
	int t=0;
	for(int i=31;i>=0;i--)
	{
		int two=(x>>i)&1;
		cnt[t]++;
		if(!node[t][two])
			node[t][two]=++ncnt;
		t=node[t][two];
	}
	cnt[t]++;
	return ;
}
LL query(LL x,int k)
{
	LL res=0;
	int u=0;
	for(int i=31;i>=0;i--)
	{
		int two=(x>>i)&1;
		if(!node[u][two^1])
			u=node[u][two];
		else if(k<=cnt[node[u][two^1]])
			u=node[u][two^1],res|=1ll<<i;
		else 
			k-=cnt[node[u][two^1]],u=node[u][two];	
	}
	return res;
}
int main ()
{
	n=read(),m=read();
	m<<=1;
	for(int i=1;i<=n;i++)
		s[i]=read(),s[i]^=s[i-1];
	for(int i=0;i<=n;i++)
		addEage(s[i]);
	for(int i=0;i<=n;i++)
		Q.push((zz){i,1,query(s[i],1)});
	LL ans=0;
	for(int i=1;i<=m;i++)
	{
		zz t=Q.top();
		Q.pop();
		ans+=t.val;
		Q.push((zz){t.id,t.k+1,query(s[t.id],t.k+1)});
	}
	ans>>=1;
	cout<<ans;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值