数据结构·并查集

前言:我坚信并查集是数据结构而非图论内容

并查集

一个可以实现合并与查询的数据结构,可以处理不相交集合的合并问题。

代码↓

int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void merge(int x,int y)
{
	int fa1=find_fa(x),fa2=find_fa(y);
	if (fa1!=fa2) fa[fa1]=fa2;
}

带权并查集

就是带权的集合相关问题,除了要维护 \(fa\) 以外还需要维护东西另外的东西(比如集合和),一般对集合的代表元操作。

扩展域并查集

这种一般是将值域翻倍(翻几倍视题中给出关系而定),对于给出的\(x,y\)关系,将不同值域上代表 \(x,y\) 的元素合并……balabala


【YbtOj】例题

A.【模板】并查集

如题干,板子题

#incIude <bits/stdc++.h>
using namespace std;
const int N=1e4+5;
const int M=1e5+5;
int n,m;
int fa[N];

int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void mge(int x,int y)
{
	int fa1=find_fa(x),fa2=find_fa(y);
	if (fa1!=fa2) fa[fa1]=fa2;
}
int main()
{
	scanf("%d%d",&n,&m);
	for (int i=0;i<=n;i++) fa[i]=i;
	while (m--)
	{
		int z,x,y;
		scanf("%d%d%d",&z,&x,&y);
		if (z==1) mge(x,y);
		if (z==2)
		{
			if (find_fa(x)!=find_fa(y)) printf("N\n");
			else printf("Y\n");
		}
	}
	return 0;
}

B.银河英雄传说

一个带权并查集。可以用 \(f_{i}\) 维护 \(i\)\(fa_{i}\) 的距离,用 \(num_{i}\) 维护 \(i\) 所在集合的元素个数。每次查询 \(x,y\) 的答案就是 \(f_{y}-f_{x-1}\) ,然后做完了

#incIude <bits/stdc++.h>
using namespace std;
const int N=3e4+5;
int T;
int fa[N];
int f[N],num[N];

int find_fa(int x)
{
	if (fa[x]==x) return x;
	int fn=find_fa(fa[x]);
	f[x]+=f[fa[x]];
	return fa[x]=fn;
}
void mrg(int x,int y)
{
	int fa1=find_fa(x),fa2=find_fa(y);
	f[fa1]+=num[fa2];//?
	fa[fa1]=fa2;
	num[fa2]+=num[fa1];
	num[fa1]=0;
}
int main()
{
	for (int i=1;i<N;i++)
	{
		fa[i]=i;
		num[i]=1;
	}
	cin>>T;
	while (T--)
	{
		char c;
		int x,y;
		cin>>c>>x>>y;
		if (c=='M') mrg(x,y);
		if (c=='C')
		{
			int fa1=find_fa(x),fa2=find_fa(y);
			if (fa1!=fa2) cout<<-1<<endl;
			else cout<<abs(f[x]-f[y])-1<<endl;
		}
	}
	return 0;
}

C.食物链

一个扩展域并查集,共有三类动物,所以扩三倍。对于 \(x\in [1,n]\),我们用\(x+n\)表示 \(x\) 的猎物,用 \(x+2n\) 表示 \(x\) 的天敌,每次给出一个关系,就将两者对应的关系放在一个集合中即可,要注意它们所牵扯的关系也要合并(例如 \(x,y\) 是同类,那么 \(x\) 的猎物也是 \(y\) 的猎物(天敌同理),需要合并。

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5;
int n,k;
int fa[N*3];//i是自己,i+n是猎物,i+n*2是天敌 
int cnt;

int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void _merge(int x,int y)
{
	int fa1=find_fa(x),fa2=find_fa(y);
	if (fa1!=fa2) fa[fa1]=fa2;
}
signed main()
{
	for (int i=1;i<N*3;i++) fa[i]=i;
	scanf("%lld%lld",&n,&k);
	while (k--)
	{
		int op,x,y;
		scanf("%lld%lld%lld",&op,&x,&y);
		if (x>n||y>n) {	cnt++; continue; }
		if (op==1) 
		{
			if (find_fa(x)==find_fa(y+n)||find_fa(x)==find_fa(y+n*2)) { cnt++; continue; } 
			_merge(x,y);//同类 
			_merge(x+n,y+n);
			_merge(x+n*2,y+n*2);
		}
		if (op==2)
		{
			if (x==y) { cnt++; continue; }
			if (find_fa(x)==find_fa(y)||find_fa(x+n*2)==find_fa(y)) { cnt++; continue; }
			_merge(x+n,y);
			_merge(x,y+n*2);
			_merge(x+n*2,y+n);
		}
	}
	printf("%lld",cnt);
	return 0;
}

D.超市购物

据 htc 大佬说反悔贪心也能过

贪心考虑,每次肯定是取价值最大的,取到后一定是要放在尽量靠后的位置,这样对后面的影响最小。于是乎,用 \(fa_i\) 维护时间 \(i\) 之前最后可以使用的时间(包括 \(i\) 本身),每次取一个时间点,就相当于是一次合并;若一次返回值为 0 ,就说明没有空余时间了,这个就取不了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int n;
int fa[N];
struct node { int p,d; } a[N];

bool cmp(node x,node y) {  return x.p>y.p;  }
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
} 
void _merge(int x,int y)
{
	int fa1=find_fa(x),fa2=find_fa(y);
	if (fa1!=fa2) fa[fa1]=fa2;
}
signed main()
{
	while (cin>>n)
	{
		int cnt=0;
		for (int i=1;i<N;i++) fa[i]=i;//fa[i]:位置i之前第一个空闲的位置 
		for (int i=1;i<=n;i++) cin>>a[i].p>>a[i].d;
		sort(a+1,a+1+n,cmp);
		for (int i=1;i<=n;i++)
		{
			int fa1=find_fa(a[i].d);
			if (fa1==0) continue;
			cnt+=a[i].p;
			_merge(fa1,find_fa(fa1-1));//这样下次就是走到前面找到的第一个空位 
		}
		cout<<cnt<<endl;
	}
	return 0;
}

E.逐个击破

据 sxht 大佬说题目有误

放个能过但有误的代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int n,m,k;
int ch[N];
int sum,tol;
struct node { int u,v,p; }e[N];
int fa[N];

void read()
{
	scanf("%lld%lld%lld",&n,&m,&k);
	for (int i=1,kk;i<=k;i++)
	{
		scanf("%lld",&kk);
		ch[++kk]=1;
	}
	for (int i=1;i<=m;i++)
	{
		int a,b,c;
		scanf("%lld%lld%lld",&a,&b,&c);
		e[i]={++a,++b,c};
		tol+=c;
	}
	for (int i=1;i<=n;i++) fa[i]=i;
}
bool cmp(node x,node y) {  return x.p>y.p;  }
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
signed main()
{
	read();
	sort(e+1,e+1+m,cmp);
	for (int i=1;i<=m;i++)
	{
		int u=e[i].u,v=e[i].v,p=e[i].p;
		int fa1=find_fa(u),fa2=find_fa(v);
		if (fa1==fa2) tol-=p;
		else
		{
			if (ch[fa1]&&ch[fa2]) continue;	
			tol-=p;
			if (ch[fa1]) fa[fa2]=fa1;
			else fa[fa1]=fa2;
		}
	}
	printf("%lld",tol);
	return 0;
}

F.躲避拥挤

要是输入的限制有序的话很好做,但是它无序,这很不好,所以我们离线操作它。

将限制人气值与道路人气值从小到大排序,用 \(num\) 维护和,然后直接做做完了

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e4+5;
const int M=1e5+5;
int T;
int n,m,q;
struct node{
	int u,v,w;
}e[M];
struct NODE{
	int id,x;
}que[N];
int fa[N],num[N];
int res,ans[N];

bool cmp1(node a,node b) {  return a.w<b.w;  }
bool cmp2(NODE a,NODE b) {  return a.x<b.x;  }
void init()
{
	res=0;
	sort(e+1,e+1+m,cmp1);
	sort(que+1,que+1+q,cmp2);
	memset(ans,0,sizeof ans);
	for (int i=1;i<=n;i++) fa[i]=i,num[i]=1;
}
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void _merge(int u,int v)
{
	int fa1=find_fa(u),fa2=find_fa(v);
	if (fa1==fa2) return ;
	res+=num[fa1]*num[fa2]*2; 
	fa[fa1]=fa2;
	num[fa2]+=num[fa1];
}
signed main()
{
	scanf("%lld",&T);
	while (T--)
	{
		scanf("%lld%lld%lld",&n,&m,&q);
		for (int i=1;i<=m;i++) scanf("%lld%lld%lld",&e[i].u,&e[i].v,&e[i].w);
		for (int i=1;i<=q;i++)
		{
			que[i].id=i;	
			scanf("%lld",&que[i].x);
		}
		init();		
		
		int id=1;
		for (int i=1;i<=q;i++)
		{
			while (id<=m&&e[id].w<=que[i].x) 
			{
				_merge(e[id].u,e[id].v);
				id++;
			}
			ans[que[i].id]=res;
		}
		
		for (int i=1;i<=q;i++) printf("%lld\n",ans[i]);
	}
	return 0;
}

G.约束系统

显然的扩展域并查集,但值域很不友好,所以我们给它离散化。然后就做完啦

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int T;
int n;
int fa[N<<1];
struct node{
	int u,v,op;
}q[N];
map <int,int> mp;

bool cmp(node a,node b) {  return a.op>b.op;  }
int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
void _merge(int u,int v)
{
	int fa1=find_fa(u),fa2=find_fa(v);
	if (fa1!=fa2) fa[fa1]=fa2;
}
signed main()
{
	scanf("%lld",&T);
	while (T--)
	{
		mp.clear();
		scanf("%lld",&n);
		int cnt=0;
		for (int i=1;i<=n;i++) 
		{
			scanf("%lld%lld%lld",&q[i].u,&q[i].v,&q[i].op);
			mp[q[i].u]=++cnt , mp[q[i].v]=++cnt;
		}		

		for(int i=1;i<=cnt;i++) fa[i]=i;
		sort(q+1,q+1+n,cmp);
		bool flag=false;
		for (int i=1;i<=n;i++)
		{
			if (q[i].op==1) _merge(mp[q[i].u],mp[q[i].v]);
			else if (find_fa(mp[q[i].u])==find_fa(mp[q[i].v])) {  flag=true; break;  } 
		}
		if (flag) printf("NO\n");
		else printf("YES\n");
	}
	return 0;
}

H.染色操作

线段树直接做做完了呃呃呃

线段树代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,m;
int cnt[N*4];
bool tag[N*4];

void pushup(int id)
{
	cnt[id]=cnt[id*2]+cnt[id*2+1]; 
}
void pushdown(int id)
{
	if (tag[id])
	{
		cnt[id*2]=0;
		cnt[id*2+1]=0;
		tag[id*2]=true;
		tag[id*2+1]=true;
		tag[id]=false;
	}
}
void build (int id,int l,int r)
{
	if (l==r) 
	{
		cnt[id]=1;
		return ;
	}
	int mid=l+r>>1;
	build(id*2,l,mid);
	build(id*2+1,mid+1,r);
	pushup(id);
}
void _update(int id,int l,int r,int x,int y)
{
	if (l>=x&&r<=y) 
	{
		cnt[id]=0;
		tag[id]=true;
		return ;
	}
	pushdown(id);
	int mid=l+r>>1;
	if (mid>=x) _update(id*2,l,mid,x,y);
	if (mid+1<=y) _update(id*2+1,mid+1,r,x,y);
	pushup(id);
}
signed main()
{
	scanf("%d%d",&n,&m);
	build(1,1,n);
	while (m--)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		_update(1,1,n,l,r);
		printf("%d\n",cnt[1]);
	}
	return 0;
}

I.数列询问

看到区间和,想到前缀和。用带权并查集维护点 \(i\)\(fa_i\) 的区间和\(\mod p\) 的结果,每次操作对于 \(l,r\),若它们的“祖宗”一样,说明它们可以用先前的关系表示出来,判断是否合法即可。若“祖宗”不一样,添一条关系即可。

#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int n,m,p;
int fa[N],sum[N];

int find_fa(int x)
{
	if (fa[x]==x) return x;
	int ret=find_fa(fa[x]);
	sum[x]=(sum[fa[x]]+sum[x])%p;
	return fa[x]=ret;
}
signed main()
{
	scanf("%lld%lld%lld",&n,&m,&p);
	for (int i=1;i<=n;i++) fa[i]=i;
	for (int i=1,l,r,k;i<=m;i++)
	{
		scanf("%lld%lld%lld",&l,&r,&k);
		int fa1=find_fa(l-1),fa2=find_fa(r);
		if (fa1==fa2)
		{
			if ((sum[r]-sum[l-1]+p)%p==k) continue;
			printf("%lld",i-1);
			return 0;
		}
		fa[fa2]=fa1;
		sum[fa2]=k-sum[r]+sum[l-1];
	}
	printf("%lld",m);
	return 0;
}

J.沧海桑田

这题用并查集维护连通性。二维非常不友好,所以给 \(fa\) 数组整成一维。注意到行数很少,所以可以枚举行、对每一行进行操作。对行进行操作就需要行内的连通性,于是乎用 \(mp_{i,j}\) 维护第 \(i\) 行第 \(j\) 个元素后第一个不连通的元素。

每次查询直接对查就行了。修改时,将行内每一个“海面”更为“地面”,再扩散维护连通性即可

#incIude <bits/stdc++.h>
#define int long long
#define y1 y_1
using namespace std;
const int N=55;
const int M=1e5+5;
int T;
int fa[N*M],mp[N][M+5];
bool col[N*M];
int dx[4]={0,0,-1,1},dy[4]={-1,1,0,0};
int op,x1,y1,x2,y2;

int find_fa(int x)
{
	if (fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}
int find_map(int x,int y)
{
	if (mp[x][y]==y) return y;
	return mp[x][y]=find_map(x,mp[x][y]);
}
void merge(int x,int y)
{
	int id=(x-1)*M+y;
	col[id]=true;
	for (int i=0;i<4;i++)
	{
		int xx=x+dx[i],yy=y+dy[i];
		int id0=(xx-1)*M+yy;
		if (id0>=1&&id0<=50*M&&col[id0]) fa[find_fa(id)]=find_fa(id0);
	}
}
signed main()
{
	for (int i=1;i<=50*M;i++) fa[i]=i;
	for (int i=1;i<=50;i++)
	{
		for (int j=1;j<=M;j++) mp[i][j]=j;
	}
	
	scanf("%lld",&T);
	while (T--)
	{
		scanf("%lld%lld%lld%lld%lld",&op,&x1,&y1,&x2,&y2);
		if (op==0)
		{
			if (x1>x2) swap(x1,x2);
			if (y1>y2) swap(y1,y2);
			for (int i=x1;i<=x2;i++)
			{
				int fx=find_map(i,y1);
				while (fx<=y2)
				{
					merge(i,fx);
					fx=mp[i][fx]=find_map(i,fx+1);
				}
			}
		}
		else
		{
			int id1=(x1-1)*M+y1,id2=(x2-1)*M+y2;
			if (col[id1]&&col[id2]&&find_fa(id1)==find_fa(id2)) printf("1\n");
			else printf("0\n");
		}
	}
	return 0;
}
posted @ 2024-12-14 15:50  还是沄沄沄  阅读(15)  评论(0编辑  收藏  举报