密文题解(图论+字典树)

题目大意

有一段长度为 n n n的密文,密文的每一位都可以用一个非负整数来描述,并且每一位都有一个权值 a i a_i ai。你可以操作任意多次,每次操作可以选择任意一段密文,花费选择的所有位上权值的异或和的代价获得这段密文每一位的异或和。求至少需要花费多少代价才能将密文的每一位都破解出来。

数据范围

1 ≤ n ≤ 1 0 5 , 0 ≤ a i ≤ 1 0 9 1\leq n\leq 10^5,0\leq a_i\leq 10^9 1n105,0ai109


题解

令前 i i i个未知数的异或和为 x i x_i xi,那么询问 [ l , r ] [l,r] [l,r]就是询问 x r ⊕ x l − 1 x_r\oplus x_{l-1} xrxl1的值。而知道每一个数的值等同于知道每个 x i x_i xi的值。

一开始,我们只知道 x 0 x_0 x0的值。对于一次询问 [ l , r ] [l,r] [l,r],如果在询问之前我们已经知道 x l − 1 x_{l-1} xl1的值或 x r x_r xr的值,那么询问之后我们就能知道它们两个的值分别为多少。

将每个 x i x_i xi看作点 i i i,将询问 [ l , r ] [l,r] [l,r]看作点 l − 1 l-1 l1向点 r r r连一条边,那么题目就转化为求让 0 0 0 n n n的所有点连通的最小代价,即求最小生成树。

令前 i i i a a a值的异或和为 s i s_i si,那么点 i i i到点 j j j的边的边权为 s i ⊕ s j s_i\oplus s_j sisj。考虑如何求最小生成树。

我们可以把所有 s i s_i si放在字典树上。对于字典树上的每一个节点,它有两棵子树。只需要从两棵子树中各选一个点,使它们的异或和最小,再把它们连起来,即可将这两部分中的点连通。

那怎么选点呢?我们可以暴力枚举其中一棵子树中的数,然后在另一棵子树上贪心去找与其异或和最小的数,对所有数求最小值即可。

因为每个节点只会被其每个父亲枚举一次,所以这样做的时间复杂度为 O ( n log ⁡ 2 w ) O(n\log^2 w) O(nlog2w),其中 w w w a i a_i ai的最大值。

code

#include
using namespace std;
const int N=30;
int n,tot=1,tmp,a[100005],s[100005],ch[5000005][2];
vector<int>v[5000005];
long long ans=0;
void pt(int s){
	int q=1;
	for(int i=N;i>=0;i--){
		if(!ch[q][(s>>i)&1]) ch[q][(s>>i)&1]=++tot;
		q=ch[q][(s>>i)&1];
		v[q].push_back(s);
	}
}
int find(int u,int s,int now){
	int re=0,vq;
	for(int i=now-1;i>=0;i--){
		int vq=(s>>i)&1;
		if(!ch[u][vq]){
			re|=(1<<i);
			vq^=1;
		}
		u=ch[u][vq];
	}
	return re;
}
void gt(int u,int now){
	--now;
	if(ch[u][0]) gt(ch[u][0],now);
	if(ch[u][1]) gt(ch[u][1],now);
	if(ch[u][0]&&ch[u][1]){
		tmp=1<<N;
		for(int i=0;i<v[ch[u][0]].size();i++){
			tmp=min(tmp,find(ch[u][1],v[ch[u][0]][i],now));
		}
		ans+=tmp+(1ll<<now);
	}
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		s[i]=s[i-1]^a[i];
	}
	for(int i=0;i<=n;i++) pt(s[i]);
	gt(1,N+1);
	printf("%lld",ans);
	return 0;
}

你可能感兴趣的:(题解,c++,题解)