并查集的原理、基本实现和应用

本文详细介绍了并查集的基本概念、逻辑结构、存储实现,包括初始化、查找、合并操作,以及路径压缩优化。重点展示了如何利用并查集解决实际问题,如判断无向图连通性和生成迷宫。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

并查集

一、简介

并查集(Union/Find)从名字可以看出,主要涉及两种基本操作:合并和查找。这说明初始时并查集中的元素是不相交的,经过一系列的基本操作(Union),最终合并成一个大的集合。而在某次合并之后,有一种合理的需求:某两个元素是否已经处在同一个集合中了?因此就需要Find操作。

并查集是一种不相交集合的数据结构,设有一个动态集合S={s1,s2,s3,.....sn},每个集合通过一个代表来标识,代表就是动态集合S中的某个元素。比如,若某个元素 x 是否在集合 s1 中(Find操作),返回集合 s1 的代表元素即可。这样,判断两个元素是否在同一个集合中也是很方便的,只要看Find(x)和Find(y)是否返回同一个代表即可。

为什么是动态集合S呢?因为随着Union操作,动态集合S中的子集合个数越来越少。数据结构的基本操作决定了它的应用范围,对并查集而言,一个简单的应用就是判断无向图的连通分量个数,或者判断无向图中任何两个顶点是否连通。

二、逻辑结构

并查集S={s1,s2,s3,.....sn}由若干子集合si构成,并查集的逻辑结构就是一个森林。si表示森林中的一棵子树。一般以子树的根作为该子树的代表。

三、存储结构

对于并查集的存储结构,可用一维数组和链表来实现。下面介绍一维数组的实现.

四、相关算法

1、并查集的初始化和查找(Find)

由于Find操作需要找到该子集合的代表元素,而代表元素是树根,因此需要保存树中结点的父亲。对于每一个结点,如果知道了父亲,沿着父结点不断往上查找就可以最终找到树根。

为了简单起见,假设一维数组s中的每个元素 s[i] 表示该元素 i 的父亲。这里有两个需要注意的地方:①我们用一维数组来存储并查集,数组的元素s[i]表示的是结点的父亲的位置。②数组元素的下标 i 则是结点的标识。如:s[5]=4,表示:结点5的父亲是结点4。

//初始化一个数组
int s[1000];
//查找根节点 
int Find(int x){
    int r=x;
    //返回根节点 r
    while (s[r] != r)
        r=s[r];
        return r;
}

2、并查集的合并

Union操作就是将两个不相交的子集合合并成一个大集合。简单的Union操作是非常容易实现的,因为只需要把一棵子树的根结点指向另一棵子树即可完成合并。这里的合并很随意,任给的2个节点x和y,只要用Find找到他们的根节点,随机把其中一个指向另外一个根节点就完成了合并。

// 合并2个节点
void Union(int x,int y)
{
    int fx=Find(x),fy=Find(y);
    if(fx!=fy)
    {
        s[fy]=fx;
    }
} 

3、Union/Find的改进——路径压缩

上面介绍的Union操作很随意:任选一棵子树,将另一棵子树的根指向它即完成了合并。如果一直按照上述方式合并,很可能产生一棵非常不平衡的子树。树越来越高了,此时会影响到Find操作的效率。这里介绍一种快速的Find方法:路径压缩。

路径压缩是指在Find操作进行时,使Find查找路径中的顶点的父亲都直接指向为树根(这很明显地改变了子树的高度,因为最终并查集中的数最大高度为2)。例如,Find要查找x的树根,引入i作为当前节点,j作为当前节点的父节点。每遍历一个当前节点i,就将i节点指向x的根节点r,然后当前节点i更新为其父亲j的位置,依次向上更新,直到全部完成。

// Find查找根节点
int Find(int x)
{   
    int r=x;
    while(r!=s[r])
        r=s[r];
    
    // 压缩路径
    int i=x,j;
    while(s[i]!=r)
    {
        j=s[i];
        s[i]=r;
        i=j;
    }
    return r;
}

4、整体代码实现

#include <iostream>
using namespace std;
 
int  s[1050];
bool t[1050];               //t 用于标记独立块的根结点
​
// Find查找根节点
int Find(int x)
{   
    int r=x;
    while(r!=s[r])
        r=s[r];
    
    // 压缩路径
    int i=x,j;
    while(s[i]!=r)
    {
        j=s[i];
        s[i]=r;
        i=j;
    }
    return r;
}
 
// 合并2个节点
void Union(int x,int y)
{
    int fx=Find(x),fy=Find(y);
    if(fx!=fy)
    {
        s[fy]=fx;
    }
} 
 
int main()
{
    int N,M,a,b,i,j,ans;
    while(scanf("%d%d",&N,&M)&&N)
    {
        for(i=1;i<=N;i++)          //初始化 
            s[i]=i;
        
        for(i=1;i<=M;i++)          //吸收并整理数据 
        {
            scanf("%d%d",&a,&b);
            Union(a,b);
        }
​
        memset(t,0,sizeof(t));
        for(i=1;i<=N;i++)          //标记根结点
        {
            t[Find(i)]=1;
        }
        for(ans=0,i=1;i<=N;i++)
            if(t[i])
                ans++;
                
        printf("%d\n",ans-1);
        
    }
    return 0;
}

五、应用场景及例子

1、应用场景

并查集数据结构非常简单,基本操作也很简单,但是用途比较大。比如,求解无向图中连通分量的个数,生成迷宫。这些应用本质上就是:初始时都是一个个不连通的对象,经过一步步处理,变成连通的了。如迷宫,初始时,起点和终点不连通,随机地打开起点到终点路径上的一个方向,直至起点和终点连通了,就生成了一个迷宫。如无向图的连通分量个数,初始时,将无向图中各个顶点视为不连通的子集合,对图中每一条边,相当于union这条边对应的两个顶点分别所在的集合,直至所有的边都处理完后,还剩下的集合的个数即为连通分量的个数。

2、例子

题目:

班上有N名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知A是B的朋友,B是C的朋友,那么我们可以认为A也是C的朋友。所谓的朋友圈,是指所有朋友的集合。

给定一个N * N的矩阵M,表示班级中学生之间的朋友关系。如果Mi= 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

实例:

输入: 
[[1,1,0],
 [1,1,0],
 [0,0,1]]
输出: 2 
说明:已知学生0和学生1互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回2。

思路:这道题目转化为求图的连通分量,可以用bfs做,但是并查集dsu来做更简单。

//先给出并查集的封装类,这里不仅封装了union、find操作,还封装了一个size计算有多少种类。
package dsu;
public class UnionFind {
   public int[] parent;
   // 初始化并查集
   public UnionFind(int n){
       parent = new int[n];
       for (int i = 0; i < n; i++) {
           parent[i] = i;
       }
   }
   // 并查集查找根
   public int find(int x){
       while (x != parent[x]){
           parent[x] = parent[parent[x]];//将祖父节点作为父亲节点
           x = parent[x];//自己升级为父亲节点,达到路径压缩的效果
       }
       return x;
   }
   // 并查集的合并
   public void union(int p,int q){
       int rootP = find(p);
       int rootQ = find(q);
       if(rootP != rootQ) parent[rootP] = rootQ;//合并为一个根
   }
   
   public int size(){//union完成之后,size可以返回图的连通分量的个数
       int r = 0;
       for (int i = 0; i < parent.length; i++) {
           if(i == parent[i]) r++;//索引等于数组元素为这一种类的根
       }
       return r;
   }
}
​
// 计算最后的size也就是朋友圈个数
public int findCircleNum(int[][] M) {
    UnionFind uf = new UnionFind(M.length);
    for (int i = 0; i < M.length; i++) {
        for (int j = 0; j < i; j++) {//根据对称性,遍历一半即可            
            if(M[i][j]==1) uf.union(i,j);//邻接矩阵元素为1,表示相识,可以union到一起
          }
      }
    return uf.size();//union完之后size就是朋友圈的个数
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值