定义
并查集是一种可以动态维护若干个不相交的集合,并支持合并和查询两种操作的一种数据结构。
基础操作
- 合并(Union):把两个不相交的集合合并为一个集合。
- 查询(Find):查询两个元素是否在同一个集合中。
引入
并查集的重要思想在于,用集合中的一个元素代表集合。
我们先来看看并查集最直接的一个应用场景:亲戚问题。
(洛谷P1551)亲戚
题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。
输入格式
第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。
以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有亲戚关系。
接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。
输出格式
P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。
我们可以建立模型,把所有人划分到若干个不相交的集合中,每个集合里的人彼此是亲戚。为了判断两个人是否为亲戚,只需看它们是否属于同一个集合即可。因此,这里就可以考虑用并查集进行维护了。(使用代表元素表示集合)
(这个比喻为转载的)现在我们把集合比作帮派,而代表元素为帮主。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架,输了就认对方为帮主。
一开始各自为战,他们的帮主就是自己。
1号和3号比武,设1号赢了(暂时这里胜负不重要),那么3号认1号为帮主(合并1号和3号所在集合,1号为代表元素)
现在2号想和3号比武(合并3号和2号所在的集合),但3号表示,别跟我打,让我帮主来收拾你(合并代表元素)。不妨设这次又是1号赢了,那么2号也认1号做帮主。
现在我们假设4、5、6号也进行了一番帮派合并,江湖局势变成下面这样:
现在假设2号想与6号比,跟刚刚说的一样,喊帮主1号和4号出来打一架(帮主真辛苦啊)。1号胜利后,4号认1号为帮主,当然他的手下也都是跟着投降了。
我们获得了一个树状结构
要找代表元素,只需要一层层向上访问父节点,直到树的根节点。
由此我们写出最简单的并查集代码
初始化
#假设有编号1,2,3...n的n个元素,用fa[]来存储父节点,一开始设为自己
fa=[0 for _ in range(n+1)]
def init(n):
for i in range(1,n+1):
fa[i]=i
查询
def find(x):
if fa[x]==x:
return x #x的父节点为自己,即x为根节点。
else:
return find(fa[x]) #x的父节点不是自己,找x的父节点的父节点,一层层向上找根节点。
合并
#找到两个集合的代表元素,将前者的父节点设为后者,这里暂时不重要设前者还是后者
def merge(i,j):
fa[find(i)]=find(j)
路径压缩
最简单的并查集的效率比较低,查询时要不断向上找代表元素,可能会形成一条长链,随着链越来越长,想从底部找到根节点越来越难。
此时,使用路径压缩,因为我们只关注每个元素对应的根节点,所以我们希望每个元素到根节点的路径尽可能短,最好是一步
查询(路径压缩)
#只要在查询的过程中,把沿途的每个节点的父节点都设为根节点,下次查询时省很多事
def find(x):
if fa[x]==x:
return x
else:
fa[x]=find(fa[x])
return fa[x]
优化后时间复杂度已经比较低了,但对于一些时间卡的很紧的题目,还可以进一步优化。
合并时,我们有一棵较复杂的树需要与一个单元素的集合合并
那么,我们选7的父节点为8好?还是设8的父节点为7好?
当然说后者好。前者会使树的深度加深。
所以我们应把简单的树往复杂的树上合并。
有两种方法:按秩合并(rank),按大小合并(size)
路径压缩和按秩合并一起使用,时间复杂度接近 O(n) ,但是很可能会破坏rank的准确性。
秩没办法严格表示树的最大深度了,会出现将未经压缩后高度为9秩为9的单链合并到未经压缩高度为2秩为10的树上。
路径压缩与按大小合并一起使用是完美兼容的,size的准确性不会被破坏。但是会出现将高度为9大小为9的树合并到高度为2大小为10的树上。不过有些场景需要查询大小,使用这个就更方便些。
初始化(按秩合并)
fa=[0 for _ in range(n+1)]
rank=[1 for _ in range(n+1)]
def init(n):
for i in range(1,n+1):
fa[i]=i
合并(按秩合并)
def mergerank(i,j):
x=find(i)
y=find(j)
if rank[x]<=rank[y]:
fa[x]=y
else:
fa[y]=x
if rank[x]==rank[y] and x!=y:
rank[y]+=1
初始化(按大小合并)
fa=[0 for _ in range(n+1)]
size=[1 for _ in range(n+1)]
def init(n):
for i in range(1,n+1):
fa[i]=i
合并(按大小合并)
def mergesize(i,j):
x=find(i)
y=find(j)
if x!=y:
if size[x]<=size[y]:
fa[x]=y
size[y]+=size[x]
else:
fa[y]=x
size[x]+=size[y]
亲戚问题(完整代码)
def init(n):
for i in range(1,n+1):
fa[i]=i
def find(x):
if fa[x]==x:
return x
else:
fa[x]=find(fa[x])
return fa[x]
def mergerank(i,j):
x=find(i)
y=find(j)
if rank[x]<=rank[y]:
fa[x]=y
else:
fa[y]=x
if rank[x]==rank[y] and x!=y:
rank[y]+=1
#根节点不同才合并
def mergesize(i,j):
x=find(i)
y=find(j)
if x!=y:
if size[x]<=size[y]:
fa[x]=y
size[y]+=size[x]
else:
fa[y]=x
size[x]+=size[y]
n,m,p=map(int,input().split())
fa=[0 for _ in range(n+1)]
rank=[1 for _ in range(n+1)]
size=[1 for _ in range(n+1)]
init(n)
for i in range(m):
x,y=map(int,input().split())
mergesize(x,y) #按大小合并,也可用mergerank(x,y)按秩合并。
for j in range(p):
x,y=map(int,input().split())
if find(x)!=find(y):
print("No")
else:
print("Yes")
无需大小/秩数组空间的技巧
如果元素的表示不涉及负数,我们可以使用这个技巧。
在以前的说明中,初始化集合时,我们令每一个元素的父节点指向自己,即 fa[i] = i,表示这是根节点。
本技巧以 fa[i] = -1
表示根节点,并以 fa[i] 是否为负数来判断i是否为根节点。
需要注意的是,因其为负数,在两棵树比较大小或秩(高度)时,值越小,则大小或秩(高度)越大。
初始化
fa=[-1 for _ in range(n+1)]
查询(路径压缩)
def find(x):
if fa[x]<0: #fa[x]<0时即为根节点
return x
else:
fa[x]=find(fa[x])
return fa[x]
合并(按秩合并)
def mergerank(i,j):
x=find(i)
y=find(j)
if x!=y:
if fa[x]<=fa[y]:
fa[y]=x
else:
fa[x]=y
if fa[x]==fa[y]:
fa[y]-=1
合并(按大小合并)
def mergesize(i,j):
x=find(i)
y=find(j)
if x!=y:
if fa[x]<=fa[y]:
fa[x]+=fa[y]
fa[y]=x
else:
fa[y]+=fa[x]
fa[x]=y
亲戚问题(完整代码)
def find(x):
if fa[x]<0:
return x
else:
fa[x]=find(fa[x])
return fa[x]
def mergesize(i,j):
x=find(i)
y=find(j)
if x!=y:
if fa[x]<=fa[y]:
fa[x]+=fa[y]
fa[y]=x
else:
fa[y]+=fa[x]
fa[x]=y
def mergerank(i,j):
x=find(i)
y=find(j)
if x!=y:
if fa[x]<=fa[y]:
fa[y]=x
else:
fa[x]=y
if fa[x]==fa[y]:
fa[y]-=1
n,m,p=map(int,input().split())
fa=[-1 for _ in range(n+1)]
for i in range(m):
x,y=map(int,input().split())
mergerank(x,y)
for j in range(p):
x,y=map(int,input().split())
if find(x)!=find(y):
print("No")
else:
print("Yes")