NOIP2023模拟19联测40 异或连通

异或查询下的连通点对计数
文章描述了一种算法,解决了一个关于图中边的存在性查询问题,通过异或运算和字典树结构,快速找出给定K条件下,哪些边存在并计算连通点对的数量。利用并查集进行贡献维护和路径删除,实现高效查询。

题目大意

有一个有nnn个点mmm条边的图,第iii条边有一个边权cic_ici

给定KKK,有qqq次询问,每次给出一个xxx,如果ci⊕x<Kc_i\oplus x<Kcix<K,则这条边存在,否则不存在。对于每个询问,输出互相连通的点对个数(即有多少1≤i<j≤n1\leq i<j\leq n1i<jn使得i,ji,ji,j连通)。

1≤n,m≤105,0≤ci,x,K<2301\leq n,m\leq 10^5,0\leq c_i,x,K<2^{30}1n,m105,0ci,x,K<230

时间限制2000ms2000ms2000ms,空间限制512MB512MB512MB


题解

我们可以想到,比较ci⊕xc_i\oplus xcixKKK,如果ci⊕xc_i\oplus xcixKKK的前t−1t-1t1位都是相同的:

  • KKK的第ttt位为111时,ci⊕xc_i\oplus xcix的第ttt位为000就一定可以取到,否则继续比较
  • KKK的第ttt位为000时,ci⊕xc_i\oplus xcix的第ttt位为000才能满足条件,还要继续往下比较

那我们把ci⊕Kc_i\oplus KciK存储到字典树中,判断哪些xxx可以使边iii存在。设当前放到了第ttt位,ci⊕Kc_i\oplus KciK的第ttt位为ppp

  • 如果KKK的第ttt位为111,则当xxx的前t−1t-1t1位和ci⊕Kc_i\oplus KciK的前t−1t-1t1位相同且x⊕cix\oplus c_ixci的第ttt位为000时(即xxx的第ttt位与cic_ici的第ttt位相同)一定可以取到,在对应子树的根节点加上这条边,然后按ppp继续往下遍历
  • 如果KKK的第ttt位为000,则继续按ppp继续往下遍历

然后,我们遍历这棵字典树,来求出每个叶子节点的答案。对于每个点,先将其能取到的边用并查集维护并计算贡献,然后遍历其子树,求完子树中叶子节点的答案之后再将这个点在图上连的边删除,所以要用带删并查集。带删并查集中要用栈来维护加入了哪些边,因为不能路径压缩,所以要用dsu on tree\text{dsu on tree}dsu on tree来保证每次查找的时间复杂度是O(log⁡n)O(\log n)O(logn)的。删边时不断删去栈顶的边并将栈顶弹出,直到这个点所有在先前加入栈中的边都被删完。

这样的话,每个叶子节点的答案都算出来了,查询就是O(1)O(1)O(1)的了。

时间复杂度为O(nlog⁡Vlog⁡n+m)O(n\log V\log n+m)O(nlogVlogn+m),其中VVV表示ci,x,Kc_i,x,Kci,x,K的值域。

可以参考代码帮助理解。

code

#include<bits/stdc++.h>
using namespace std;
const int N=100000;
int n,m,q,K,tot=1,tp=0,fa[N+5],siz[N+5];
int wh[N+5],ch[32*N+5][2];
long long now=0,ans[32*N+5];
pair<int,int>st[N+5];
vector<int>v[32*N+5][2];
struct node{
	int x,y,w;
}w[N+5];
void pt(int w,int id){
	int q,vq=1;
	for(int i=30;i>=0;i--){
		if((K>>i)&1){
			v[vq][(w>>i)&1].push_back(id);
		}
		q=((K^w)>>i)&1;
		if(!ch[vq][q]) ch[vq][q]=++tot;
		vq=ch[vq][q];
	}
}
int gt(int x){
	int q,vq=1;
	for(int i=30;i>=0;i--){
		q=(x>>i)&1;
		if(!ch[vq][q]) ch[vq][q]=++tot;
		vq=ch[vq][q];
	}
	return vq;
}
int find(int ff){
	if(fa[ff]!=ff) return find(fa[ff]);
	return ff;
}
long long gts(int x){
	return 1ll*x*(x-1)/2;
}
void merge(int x,int y){
	int v1=find(x),v2=find(y);
	if(v1==v2) return;
	if(siz[v1]<siz[v2]) swap(v1,v2);
	now-=gts(siz[v1])+gts(siz[v2]);
	fa[v2]=v1;
	siz[v1]+=siz[v2];
	st[++tp]={v1,v2};
	now+=gts(siz[v1]);
}
void del(int x,int y){
	now-=gts(siz[x]);
	siz[x]-=siz[y];
	fa[y]=y;
	now+=gts(siz[x])+gts(siz[y]);
}
void stpop(int tmp){
	while(tp>tmp){
		del(st[tp].first,st[tp].second);
		--tp;
	}
}
void solve(int u){
	ans[u]=now;
	if(ch[u][0]){
		int tmp=tp;
		for(int p:v[u][0]){
			merge(w[p].x,w[p].y);
		}
		solve(ch[u][0]);
		stpop(tmp);
	}
	if(ch[u][1]){
		int tmp=tp;
		for(int p:v[u][1]){
			merge(w[p].x,w[p].y);
		}
		solve(ch[u][1]);
		stpop(tmp);
	}
}
int main()
{
//	freopen("xor.in","r",stdin);
//	freopen("xor.out","w",stdout);
	scanf("%d%d%d%d",&n,&m,&q,&K);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&w[i].x,&w[i].y,&w[i].w);
		pt(w[i].w,i);
	}
	for(int i=1;i<=n;i++){
		fa[i]=i;siz[i]=1;
	}
	for(int i=1,x;i<=q;i++){
		scanf("%d",&x);
		wh[i]=gt(x);
	}
	solve(1);
	for(int i=1;i<=q;i++){
		printf("%lld\n",ans[wh[i]]);
	}
	return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值