题解
看到区间的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;
}