算法学习笔记(1):并查集

并查集是一种用于动态维护不相交集合的数据结构,支持合并与查询操作。文章通过亲戚关系问题解释了其工作原理,包括基础的合并与查询操作,以及路径压缩、按秩合并和按大小合并的优化技术。并提供了完整的亲戚问题代码示例,展示了如何利用并查集解决此类问题。
摘要由CSDN通过智能技术生成

定义

并查集是一种可以动态维护若干个不相交的集合,并支持合并查询两种操作的一种数据结构。

基础操作

  • 合并(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")

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值