并查集

题单

这个专辑之前写在洛谷上,但年久失修,所以我决定在博客上重写一遍

正文

定义

并查集,顾名思义,是关于合并以及查询的集合,实现为一个森林,每一棵树是一个集合。支持两种操作,合并以及查询,合并是指将两个元素所在的两个树合并,查询则是查找一个元素所在树的根结点。

初始化

在没有任何操作进行的时候,所有元素所在树的根结点都是自己

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

查询

对于每个要查询的结点,向上寻找 \(fa[x]\),直到某一层上 \(fa[x]=x\) 为止。答案就是当时的 \(x\)。这部分通常用递归的方式完成

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

合并

路径压缩

由于我们只关心每颗树根结点的信息,所以直接让一棵树中的所有点全部连接到根结点上,即为路径压缩。
这一步类似于记忆化搜索,只需要在查询时让 \(fa[x]\) 指向根节点即可。

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

合并操作

同理,由于我们只关心每颗树根结点的信息,直接让 \(A\) 子树的根结点连到 \(B\) 子树的根结点即可。

void merge(int x,int y)
{
	int fx=find(x),fy=find(y);
	fa[fx]=fy;
}

Luogu P3367

题意简述

给定 \(n\) 个节点,初始每个节点自成一个集合。
共有 \(m\) 次操作,分为两种:

  • \(1\ x\ y\):合并 \(x,y\) 所在集合;
  • \(2\ x\ y\):查询 \(x,y\) 是否在同一集合。

若在同一集合输出 \(Y\),否则输出 \(N\)

解法

并查集模板题仅需上述两种操作即可通过

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int fa[N];
int find(int x)
{
	if(x==fa[x])
	{
		return x;
	}
	return fa[x]=find(fa[x]);
}
int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		fa[i]=i;
	}
	while(m--)
	{
		int op,x,y;
		cin>>op>>x>>y;
		if(op==1)
		{
			fa[find(x)]=find(y);
		}
		else
		{
			int fx=find(x),fy=find(y);
			if(fx==fy)
			{
				cout<<"Y"<<endl;
			}
			else
			{
				cout<<"N"<<endl;
			}
		}
	}
	return 0;
}

Luogu P1111

简述题意

\(n\) 个点,\(m\) 条未连接的边,每条边在第 \(t_i\) 时间可以通车,求最少需要多少时间能使任意两点通车

解法

因为我们要求最小时间,可以把边按时间排序,并循环 \(n\) 次,每次对并查集进行一次合并操作,直到所有点都在一个集合为止。

代码

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int fa[N],sz[N];
int find(int x)
{
	if(x==fa[x])
	{
		return x;
	}
	return fa[x]=find(fa[x]);
}
struct node
{
	int u,v,w;
	void read()
	{
		cin>>u>>v>>w;
	}
}g[N];
bool cmp(node n1,node n2)
{
	return n1.w<n2.w;
}
int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		fa[i]=i;
		sz[i]=1;
	}
	for(int i=1;i<=m;i++)
	{
		g[i].read();
	}
	sort(g+1,g+m+1,cmp);
	for(int i=1;i<=m;i++)
	{
		int u=g[i].u,v=g[i].v,w=g[i].w;
		int fu=find(u),fv=find(v);
		if(fu==fv)
		{
			continue;
		}
		fa[fu]=fv;
		sz[fv]+=sz[fu];//用sz数组统计每个集合的元素数量 
		if(sz[fv]==n)
		{
			cout<<w<<endl;
			return 0;
		}
	}
	cout<<-1<<endl; 
	return 0;
}

Luogu P1525

题意简述

有 \(n\) 名罪犯,需要关进两座监狱。
存在 \(m\) 组罪犯关系,每组关系拥有冲突权值 \(w\),两人在同一监狱则产生 \(w\) 的冲突。

解法

这道题是扩展域并查集的模板题

扩展域并查集

定义

扩展域并查集不同于普通并查集,普通并查集只能表示合并关系,而扩展域并查集可以表示不在一个集合内等等的其他关系

使用

使用扩展域并查集非常简单,对于你想要表达的 \(k\) 种关系,只需要将 \(fa\) 数组开成 \(n\times k\) 倍的空间即可,表达第 \(x\) 种关系则只需要把 \(now\) 和 \(now+(x-1)\times n\) 合并即可

本题解法

由于题目需要求最小值,考虑排序,每次拿到一个边 \((u,v)\),如果 \(u\) 和 \(v\) 不在同一个集合里,则将它们标记为不能放在一个集合内,如果在一个集合里,边权 \(w\) 即为答案

代码

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct node
{
	int u,v,w;
}g[N];
bool cmp(node n1,node n2)
{
	return n1.w>n2.w;
}
int fa[N];
int find(int x)
{
	if(fa[x]==x)
	{
		return x;
	}
	return fa[x]=find(fa[x]);
}
int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		cin>>g[i].u>>g[i].v>>g[i].w;
	}
	sort(g+1,g+m+1,cmp);
	for(int i=1;i<=2*n;i++)
	{
		fa[i]=i;
	}
	for(int i=1;i<=m;i++)
	{
		int x=g[i].u,y=g[i].v,w=g[i].w;
		int fx=find(x),fy=find(y);
		if(fx==fy)
		{
			cout<<w<<endl;
			return 0;
		}
		int fxn=find(x+n),fyn=find(y+n);
		if(fxn!=fy)
		{
			fa[fxn]=fy;
		}
		if(fyn!=fx)
		{
			fa[fyn]=fx;
		}
	}
	cout<<0<<endl;
	return 0;
}

Luogu P2024

题意

存在 \(n\) 个物种,满足环形食物链关系:\(x\) 吃 \(y\),\(y\) 吃 \(z\),\(z\) 吃 \(x\)。

给定 \(k\) 条语句,分为两类:

  • \(1\ x\ y\):\(x,y\) 为同类;
  • \(2\ x\ y\):\(x\) 吃 \(y\)。

若语句不合法(越界 / 与前文矛盾)则判定为假话,最终输出假话总数。

解法

这道题和上道题基本没有什么区别,只需要想明白就能AC。
扩展域:\(x\) 是自己,\(x+n\) 是被吃,\(x+2\times n\) 是吃别人。

代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+10;
int fa[N*3];
int find(int x)
{
	if(fa[x]==x)
	{
		return x;
	}
	return fa[x]=find(fa[x]);
}
signed main()
{
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=3*n;i++)
	{
		fa[i]=i;
	}
	int cnt=0;
	while(k--)
	{
		int op,x,y;
		cin>>op>>x>>y;
		if(x>n||y>n)
		{
			cnt++;
			continue;
		}
		if(op==1)
		{
			if(find(x)==find(y+n)||find(x)==find(y+n*2))
			{
				cnt++;
				continue;
			}
			if(find(x)!=find(y))
			{
				fa[find(x)]=find(y);
			}
			if(find(x+n)!=find(y+n))
			{
				fa[find(x+n)]=find(y+n);
			}
			if(find(x+2*n)!=find(y+2*n))
			{
				fa[find(x+2*n)]=find(y+2*n);
			}
		}
		if(op==2)
		{
			if(x==y)
			{
				cnt++;
				continue;
			}
			if(find(x)==find(y)||find(x)==find(y+2*n))
			{
				cnt++;
				continue;
			}
			if(find(x)!=find(y+n))
			{
				fa[find(x)]=find(y+n);
			}
			if(find(x+n)!=find(y+2*n))
			{
				fa[find(x+n)]=find(y+2*n);
			}
			if(find(x+2*n)!=find(y))
			{
				fa[find(x+2*n)]=find(y);
			}
		}
	}
	cout<<cnt<<endl;
	return 0;
}

Luogu P1892

题意

共有 \(n\) 个人,满足规则:
朋友的朋友是朋友,敌人的敌人是朋友。

给定 \(k\) 组关系:

  • \(F\ x\ y\):\(x,y\) 是朋友(同集合);
  • \(E\ x\ y\):\(x,y\) 是敌人(对立关系)。

求最终一共存在多少个独立团伙。

解法

这道题和关押囚犯差不多,如果两个人是朋友,则直接合并,若两人是敌人,则用扩展域并查集标记两人为敌人

代码

#include <bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int fa[N*2];
int find(int x)
{
	if(fa[x]==x)
	{
		return x;
	}
	return fa[x]=find(fa[x]);
}
int main()
{
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=2*n;i++)
	{
		fa[i]=i;
	}
	while(k--)
	{
		char op;
		int x,y;
		cin>>op>>x>>y;
		if(op=='E')
		{
			int fx=find(x),fy=find(y),fxn=find(x+n),fyn=find(y+n);
			fa[fyn]=fx;
			fa[fxn]=fy;
		}
		else
		{
			int fx=find(x),fy=find(y);
			fa[fx]=fy;
		}
	}
	int cnt=0;
	for(int i=1;i<=n;i++)
	{
		if(i==find(i))
		{
			cnt++;
		}
	}
	cout<<cnt<<endl;
	return 0;
}

Luogu P1196

题意简述

题意概括
一共有 \(30000\) 艘战舰,初始时第 \(i\) 号战舰位于第 \(i\) 列,每列仅有一艘战舰。
总共有 \(T\) 条指令,分为两种:

  1. \(\boldsymbol{M\ i\ j}\):将战舰 \(i\) 所在的整个队列,整体接在战舰 \(j\) 所在队列的尾部。保证执行该指令时,\(i,j\) 不在同一列。
  2. \(\boldsymbol{C\ i\ j}\):查询战舰 \(i\) 和战舰 \(j\)。
    • 如果二者不在同一列,输出 \(-1\);
    • 如果二者在同一列,输出两艘战舰中间夹着的战舰数量。

带权并查集

这一类并查集不仅仅是通过点对之间的关系操作,而是要在处理关系的同时维护其他的信息。
例如这道题,就是要在合并战舰的时候维护每个战舰到战舰队首的距离和每个战舰队伍的长度。
带权并查集的主要难点是要想清楚每个信息在合并的哪一个部分修改,如果想不清楚,大概率只能拿暴力分。

解法

本题除了维护 \(fa[x]\) 外,还要额外维护两个数组:

  • \(d[x]\):路径压缩完成后,表示战舰 \(x\) 到所在队列根结点(队首)的距离。
  • \(\mathit{len}[root]\):以 \(root\) 为根的队列中战舰总个数。

合并:把 \(i\) 的根接到 \(j\) 的根尾部,令 \(d[\mathit{root}_i]=\mathit{len}[\mathit{root}_j]\),再执行 \(\mathit{len}[\mathit{root}_j]\mathrel{+}=\mathit{len}[\mathit{root}_i]\)。
路径压缩过程中同步更新偏移量 \(d[x]\)。

代码

本题代码需要注意,每个点初始到根结点的距离为 \(0\)

#include<bits/stdc++.h>
using namespace std;
const int N=3e4+10;
int fa[N],d[N],len[N];
int find(int x)
{
	if(x==fa[x])return x;
	int root=find(fa[x]);//先更新fa[x]再累加到每个儿子的距离上 
	d[x]+=d[fa[x]]; 
	return fa[x]=find(fa[x]);
}
int main()
{
	int T;
	cin>>T;
	for(int i=1;i<=N-10;i++)//初始化 
	{
		fa[i]=i;
		len[i]=1;
	}
	while(T--)
	{
		char op;
		int x,y;
		cin>>op>>x>>y;
		if(op=='M')
		{
			int fx=find(x),fy=find(y);
			d[fx]=len[fy];
			len[fy]+=len[fx];
			len[fx]=0;
			fa[fx]=fy; 
		}
		else if(op=='C')
		{
			int fx=find(x),fy=find(y);
			if(fx!=fy)cout<<-1<<endl;
			else cout<<(d[x]-d[y]-1>=0?d[x]-d[y]-1:d[y]-d[x]-1)<<endl; 
		}
	}
	return 0;
}
posted @ 2026-10-18 15:14  jianghaochen  阅读(11)  评论(0)    收藏  举报