数据结构学习NOTE- 并查集

并查集

并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。

顾名思义,并查集支持两种操作:

  • 合并(Union):合并两个元素所属集合(合并对应的树)

  • 查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合

常见并查集:普通并查集,扩展域并查集,带权并查集,可持久化并查集,可撤销并查集。

P3367 【模板】并查集

普通并查集

无优化,会T3个点

#include<iostream>
#include<cstdio>
#define MAXN 10000

using namespace std;

int n,m,p,x,y;
int opt;
int fa[MAXN];

int find(int x)
{
    if(x==fa[x]) return x;
    return find(fa[x]);
}

void join(int c1,int c2)
{
    int f1=find(c1),f2=find(c2);
    if(f1!=f2) fa[f1]=f2;
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)
	{
        fa[i]=i;
    }
    
    for(int i=1;i<=m;i++)
	{
    	scanf("%d",&opt);
    	if(opt==1
		){
    		scanf("%d %d",&x,&y);
    		join(x,y);
		}
		else if(opt==2)
		{
			scanf("%d %d",&x,&y);
			if(find(x)==find(y)) cout<<"Y"<<endl;
			else cout<<"N"<<endl;
		}
	}
    return 0;
}

路径压缩优化

#include<iostream>
#include<cstdio>
#define MAXN 20005

using namespace std;

int n,m,p,x,y;
int opt;
int fa[MAXN];

int find(int x)
{
    if(x==fa[x]) return x;
    return fa[x]=find(fa[x]);
}

void join(int c1,int c2)
{
    int f1=find(c1),f2=find(c2);
    if(f1!=f2) fa[f1]=f2;
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)
	{
        fa[i]=i;
    }
    
    for(int i=1;i<=m;i++)
	{
    	scanf("%d",&opt);
    	if(opt==1
		){
    		scanf("%d %d",&x,&y);
    		join(x,y);
		}
		else if(opt==2)
		{
			scanf("%d %d",&x,&y);
			if(find(x)==find(y)) cout<<"Y"<<endl;
			else cout<<"N"<<endl;
		}
	}
    return 0;
}

按秩合并

#include<iostream>
#include<cstdio>
#define MAXN 10005

using namespace std;

int n,m,p,x,y;
int opt;
int fa[MAXN],siz[MAXN];

int find(int x)
{
    if(x==fa[x]) return x;
    return fa[x]=find(fa[x]);
}

void join(int c1,int c2)
{
    int f1=find(c1),f2=find(c2);
    if(f1!=f2)
	{
		if(siz[f1]>siz[f2])
		{
			fa[f1]=f2;
			siz[f2]+=siz[f1];
		}
		else
		{
			fa[f2]=f1;
			siz[f1]+=siz[f2];
		}
	}
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)
	{
        fa[i]=i;
        siz[i]=1;
    }
    
    for(int i=1;i<=m;i++)
	{
    	scanf("%d",&opt);
    	if(opt==1
		){
    		scanf("%d %d",&x,&y);
    		join(x,y);
		}
		else if(opt==2)
		{
			scanf("%d %d",&x,&y);
			if(find(x)==find(y)) cout<<"Y"<<endl;
			else cout<<"N"<<endl;
		}
	}
    return 0;
}

路径压缩与按秩合并均可 A C AC AC,但路径压缩会快个 20 m s 20ms 20ms 左右。

P1551 亲戚

纯板子

P1892 [BOI2003] 团伙

扩展域并查集的使用。令 f a 1 − f a n fa_1-fa_n fa1fan 表示朋友域, f a n + 1 − f a 2 n fa_{n+1}-fa_{2n} fan+1fa2n 表示敌人域, F F F 操作直接合并即可。 E E E 操作将 p p p q + n q+n q+n q q q p + n p+n p+n 合并(自己与自己敌人的敌人是朋友) ,一定要将敌人域接到朋友域上来,否则答案会少!

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

const int maxn=2e3+5;
int fa[maxn];
int n,m;
char opt;int p,q;

void init()
{
	for(int i=1;i<=2*n;i++) fa[i]=i;
}

int find(int x)
{
	if(fa[x]==x) return fa[x];
	return fa[x]=find(fa[x]);
}

void join(int c1,int c2)
{
	int x1=find(c1),x2=find(c2);
	if(x1!=x2) fa[x2]=x1;
}

int main()
{
	#ifndef ONLINE_JUDGE
	//freopen("in.txt","r",stdin);
	#endif
	cin>>n>>m;
	init();
	for(int i=1;i<=m;i++)
	{
		cin>>opt>>p>>q;
		if(opt=='F')
		{
			join(p,q);
		}
		else join(p,q+n),join(q,p+n);
	}
	int cnt=0;
	for(int i=1;i<=n;i++) if(fa[i]==i) cnt++;
	cout<<cnt;
	return 0;
}

P1196 [NOI2002] 银河英雄传说

带权并查集。带权并查集特征:多维护一个数组 d d d d x d_x dx 表示 x x x 到根节点的权值。合并和路径压缩时注意如何更新维护的数组。

路径压缩关键代码:

写法1

int find(int x)
{
	if(x!=fa[x])
	{
		int rt=find(x);//直接找到根节点接上去
		d[x]+=d[fa[x]];
		fa[x]=rt;
	}
	return fa[x];
}

写法2

int find(int x)
{
	if(x!=fa[x])
	{
		int t=fa[x];
		fa[x]=find(fa[x]);
		d[x]+=d[t];
	}
	return fa[x];
}

于是此题考虑维护两个数组。其中 d d d 表示该战舰到根的距离, s s s 表示战舰群的长度,这样处理答案显然。处理过程如图:

过程

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

const int maxn=30005;
int T;
int fa[maxn],d[maxn],s[maxn];
char op;int s1,s2;

void init()
{
	for(int i=1;i<maxn;i++) fa[i]=i,s[i]=1;
}

int find(int x)
{
	if(x!=fa[x])
	{
		int t=fa[x];
		fa[x]=find(fa[x]);
		d[x]+=d[t];
	}
	return fa[x];
}

void join(int s1,int s2)
{
	int x1=find(s1),x2=find(s2);
	if(x1!=x2)
	{
		d[x1]=s[x2];
		s[x2]+=s[x1];
		fa[x1]=x2;
	}
}

int main()
{
	#ifndef ONLINE_JUDGE
	freopen("in.txt","r",stdin);
	#endif
	cin>>T;
	init();
	while(T--)
	{
		cin>>op>>s1>>s2;
		if(op=='M')
		{
			join(s1,s2);
//			for(int i=1;i<=n;i++) cout<<fa[i]<<" ";cout<<endl;
//			for(int i=1;i<=n;i++) cout<<d[i]<<" ";cout<<endl;
//			for(int i=1;i<=n;i++) cout<<s[i]<<" ";cout<<endl;
		}
		else
		{
			int x1=find(s1),x2=find(s2);
			if(x1!=x2) cout<<-1<<endl;
			else cout<<max(abs(d[s1]-d[s2])-1,0)<<endl;
		}

	}
	return 0;
}

P1955 [NOI2015] 程序自动分析

主要思想:相等的数在同一个集合,不相等的数在一个集合就会有矛盾。

离散化+并查集即可。

P1197 [JSOI2008] 星球大战

经典倒序处理+连通块判断。

因为并查集的删除操作不好做,于是考虑从最后情况开始,不断建边直到初始状况,记录连通块数量便是答案。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

const int maxn=2e6+5;
int n,m,k;
int fa[maxn],ans[maxn];
bool f[maxn];
int brok[maxn];


void init()
{
	for(int i=0;i<=n;i++)
	{
		fa[i]=i;
	}
}

int find(int x)
{
    if(x==fa[x]) return x;
    return fa[x]=find(fa[x]);
}
int tot;
void join(int c1,int c2)
{
    int f1=find(c1),f2=find(c2);
    if(f1!=f2)
    {
    	--tot;fa[f1]=f2;//每一次合并操作减少1个连通块
    }
}
vector<int> G[maxn];

int main()
{
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		G[x].push_back(y);
		G[y].push_back(x);
	}
	cin>>k;
	for(int i=1;i<=k;i++)
	{
		scanf("%d",&brok[i]);
		f[brok[i]]=1;
	}
	init();
	tot=n-k;
	// cout<<tot<<endl;
	for(int i=0;i<n;i++)
	{
		if(!f[i])
		{
			for(int j=0;j<G[i].size();j++)
			{
				if(!f[G[i][j]])
				{
					join(i,G[i][j]);
					// --tot;
				}
			}
		}
	}
	for(int i=k;i>=1;i--)
	{
		ans[i]=tot;
		int now=brok[i];
		++tot,f[now]=0;//倒推,增加一个连通块
		for(int j=0;j<G[now].size();j++)
		{
			int v=G[now][j];
			if(f[v])
			{
				continue;
			}
			join(now,v);
			// --tot;
			// tot=max(1,tot);
		}
	}
	ans[0]=tot;
	for(int i=0;i<=k;i++) cout<<ans[i]<<endl;
	return 0;

}

4287. 导航噩梦

  • 题目大意:

    给定 n n n 个坐标的 m m m 个相对关系,求出输入的第 i i i 条语句时两个坐标的曼哈顿距离。

  • 题目分析:

    首先关于每个农场是否连通,可用普通并查集解决。

    而维护每个点之间的曼哈顿距离,则想到用带权并查集,使用数组 d x , d y dx,dy dx,dy 分别维护点 i i i 到跟节点的横纵坐标之差 d x i , d y i dx_i ,dy_i dxi,dyi

    本题要求到第 i i i 条信息后的情况,考虑询问按照 i i i 排序,离线处理。

  • 具体实现:

    基本思路已经明确了,接下来就是看如何操作,主要是看怎样维护带权并查集。

    这里都是向量的运算,建议熟悉这方面知识。

    1. 修改操作
      d = E d=\texttt{E} d=E 时举例:

      假设 a a a 节点的祖先为 p a pa pa b b b 节点的祖先为 p b pb pb

      我们不妨将 p a pa pa 集合合并到 p b pb pb 上。此时便考虑如何更新 d x p a , d y p a {dx}_{pa},{dy}_{pa} dxpa,dypa

      根据下面这张图

      可以得出此时 d x p a = d x b + l − d x a {dx}_{pa}={dx}_b+l-dx_a dxpa=dxb+ldxa

      d y dy dy及剩余的操作同理可以得出。

    2. 路径压缩

      根据此图,将 f a x fa_x fax 直接合并到根节点上,此时接可以直接更新 d x , d y d_x,d_y dx,dy 了。

      dx[x]+=dx[fa[x]];
      dy[x]+=dy[fa[x]];
      

      x x x 到根的距离分成两段求有以上公式。

  • AC_code

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int maxn=40005;
int n,m,k;
char d;

int fa[maxn],dx[maxn],dy[maxn];

void init(int n)
{
	for(int i=1;i<=n;i++)
	{
		fa[i]=i,dx[i]=0,dy[i]=0;
	}
}
int find(int x)//路径压缩
{
	if(fa[x]!=x)
	{
		int rt=find(fa[x]);
		dx[x]+=dx[fa[x]];
		dy[x]+=dy[fa[x]];
		fa[x]=rt;
	}
	return fa[x];
}

struct node
{
	int a,b,i;
	int id;
}qus[maxn];
bool cmp(node a,node b)
{
	return a.i<b.i;//依照i排序
}

struct opt
{
	int a,b,l;
	char d;
	
}op[maxn];
int ans[maxn],cnt=0;

int main()
{
	#ifndef ONLINE_JUDGE
 	freopen("in.txt","r",stdin);
 	#endif
 	cin>>n>>m;
 	init(n);
 	for(int i=1;i<=m;i++)
 	{
	 	scanf("%d%d%d",&op[i].a,&op[i].b,&op[i].l);
	 	cin>>op[i].d;
	}
	cin>>k;
	for(int i=1;i<=k;i++)
	{
		scanf("%d%d%d",&qus[i].a,&qus[i].b,&qus[i].i);
		qus[i].id=i;
	}
	//读入,方便离线处理
	sort(qus+1,qus+k+1,cmp);
	int now=1;
	for(int i=1;i<=k;i++)
	{
		
		while(now<=qus[i].i)
		{
			char d=op[now].d;
			int a=op[now].a,b=op[now].b,l=op[now].l;
			int pa=find(a),pb=find(b);
			if(pa==pb) continue;
			if(d=='E')//依照dx,dy的定义合并+维护
		 	{
			 	fa[pa]=pb;
			 	dx[pa]=dx[b]+l-dx[a];
			 	dy[pa]=dy[b]-dy[a];
			}
			else if(d=='S')
			{
				fa[pa]=pb;
				dy[pa]=dy[b]+l-dy[a];
				dx[pa]=dx[b]-dx[a];
			}
			else if(d=='W')
			{
				fa[pa]=pb;
				dx[pa]=dx[b]-l-dx[a];
				dy[pa]=dy[b]-dy[a];
			}
			else if(d=='N')
			{
				fa[pa]=pb;
				dy[pa]=dy[b]-l-dy[a];
				dx[pa]=dx[b]-dx[a];
			}
			now++;
		}
		//按照i的顺序离线处理(离线的原因之一是并查集的删除操作并不好做)
		int a=qus[i].a,b=qus[i].b;
		int pa=find(a),pb=find(b);
		if(pa!=pb)
		{
			ans[qus[i].id]=-1;
		}
		else
		{
			ans[qus[i].id]=abs(dx[a]-dx[b])+abs(dy[a]-dy[b]);
		}
	}
	for(int i=1;i<=k;i++)
	{
		cout<<ans[i]<<endl;
	}
	return 0;
}

P4185 [USACO18JAN] MooTube G

同样并查集+连通块。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值