P5283 异或粽子 题解

题意:

给定一个长度为 \(n\) 的整数数组 \(a\),求 \(a\) 中前 \(k\) 大的异或区间和之和。

首先对 \(a\) 处理一个异或前缀和数组 \(s\),这样区间 \([l,r]\) 的异或和就转化成了 \(s_{l-1} \oplus s_r\)。题目就变为求 \(n+1\) 个数(包含一个空前缀)两个数异或前 \(k\) 大的值的和。

直接全部枚举然后排序显然会超时,这里我们可以用到一个 trick:对于一个前缀 \(s_x\),一定有与它异或最大的,次大的……第 \(n\) 大的前缀。首先最大的异或值一定是在所有前缀对应的最大值中的最大值,我们取出这个值,次大的异或值一定出自未取的最大值,以及取走的那个前缀的次大值,以此类推。

所以重新审视一下需求:我们需要一个支持快速插入,删除,取最优的数据结构,这里使用堆来维护。

至于如何求出若干个数与一个数异或值第 \(k\) 大的异或值:

首先我们都知道有一道典题:求出若干个数与一个数异或值最大的异或值。

我们将这些数都插入一棵 \(01\) trie,能走反就走反。为了解决这个延申问题,我们同样将这些数都插入一棵 \(01\) trie,但同时处理出每个子树的大小。当一个点有两个儿子时,若其取反子树的大小 \(siz1\) 大于等于查找的排行 \(rk\),我们就走取反。否则走不取反的子树,在不取反的子树中查找第 \(rk-siz1\) 大的值。

当然最后我们会发现一个问题:比如一对数 \(s_i,s_j\),我们会将 \(i\) 匹配 \(j\) 计算一次贡献,也会将 \(j\) 匹配 \(i\) 计算一次贡献,我们只需要查询前 \(2k\) 个数,最后除以二即可。

点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define debug cout<<"DEBUG"<<endl;
#define pb push_back
#define pii pair<int,int>
#define vi vector<int>
#define imp map<int,int>
using namespace std;
const int N=5e5+5,M=2e7+5; 
int n,m,son[M][2],siz[M],cnt;
ll a[N],s[N];
struct node{
	ll x,rk,val;
	bool operator <(const node &k) const {
		return val<k.val;
	}
};
priority_queue<node>pq;
void insert(ll x){
	int p=0;
	for(int i=31;i>=0;i--){
		int now=(x>>i)&1;	
		siz[p]++;
		if(!son[p][now]){
			son[p][now]=++cnt;
		}
		p=son[p][now];
	}
	siz[p]++;
}
ll query(ll x,ll rk){
	int p=0;
	ll ans=0;
	for(int i=31;i>=0;i--){
		int now=(x>>i)&1;
		if(!son[p][now^1]){
			p=son[p][now];
		}else if(rk<=siz[son[p][now^1]]){
			ans+=(1ll<<i);
			p=son[p][now^1];
		}else{
			rk-=siz[son[p][now^1]];
			p=son[p][now];
		}
	}
	return ans;
}
int main(){
	cin>>n>>m;
	m*=2;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		s[i]=s[i-1]^a[i];
	}
	for(int i=0;i<=n;i++){
		insert(s[i]);
	}
	for(int i=0;i<=n;i++){
		pq.push({i,1,query(s[i],1)});
	}
	ll ans=0;
	while(m--){
		node t=pq.top();
		pq.pop();
		ans+=t.val;
		if(t.rk<n) pq.push({t.x,t.rk+1,query(s[t.x],t.rk+1)});
	}
	cout<<ans/2<<"\n";
	return 0;
}
posted @ 2023-05-16 21:59  Aurora_Borealis  阅读(30)  评论(0)    收藏  举报