并 查 集

并查集(union-find sets)是一种精巧而实用的数据结构,主要用于求一些不相关的集合的合并问题。常见的一些问题如求子连通图,(求最小生成树的 Kruskal 算法和求最近公共祖先(Least Common Ancestors, LCA)等。)。

使用并查集时,首先会存在一组不相关的动态集合,一般会使用一个整数代表一个集合中的元素。

每个集合中使用谁做集合的代表都是可以的,主要是要可以迅速寻找到是否几个元素存在一个集合中,给定指定的元素可以迅速判断这个元素属于哪个集合。(我们关心的是,对于给定的元素,可以很快的找到这个元素所在的集合(的代表),以及合并两个元素所在的集合,而且这些操作的时间复杂度都是常数级的。)

并查集的三个基本操作:

1.makeset():初始化给定的元素,以自身作为祖先(根节点)。(建立一个新的并查集,其中包含 s 个单元素集合。)

2.unionset(x,y):将不同集合中的元素根据关系连通成为一个集合,即把元素 x 和元素 y 所在的集合合并,要求 x 和 y 所在的集合不相交,如果相交则不合并。

3.find(x):对于给定的元素进行查找,并返回其所在集合的代表。(该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。)

并查集优化即及代码:

1.初始化

复制代码
 1 int fa[maxn];//根节点
 2 int rank[maxn];//高度
 3 int n,m,t;
 4 void makeset(){
 5     for(int i=1; i<=n; i++)//初始化 
 6     {
 7         fa[i]=i;//初始时每个集合的父节点都是自己,即自己代表自己
 8         rank[i]=0;//集合只有自己,树高为0
 9     }
10 } 
复制代码

2.合并操作

一般的合并操作就是将一个集合的根指向另一个集合,而没有方向。

这里有一种启发式策略---按秩合并。按秩合并是将较低秩的集合指向较高秩的集合,(该方法使用秩来表示树高度的上界,)即在合并时,将树高较低的指向较高的集合。

为了保存秩,需要额外使用一个于fa[]相同的大小的数组保存,并初始化为零。

复制代码
 1 void unionset(int x, int y){//按秩合并 
 2     x=find(x);
 3     y=find(y);
 4     if(x==y) return ;
 5     if(rank[x]<rank[y])//比较秩的大小 
 6         fa[x]=y;
 7     else    //rank[x]>=rank[y] 
 8     {
 9         fa[y]=x;
10         if(rank[x]==rank[y]) //在秩(树高度)相等时,将x当作y的祖先,x的高度加一 
11             rank[x]++;
12     }
13     
14 }
复制代码

(摘自一位大佬   CYJB  )除了按秩合并,并查集还有一种常见的策略,就是按集合中包含的元素个数(或者说树中的节点数)合并,将包含节点较少的树根,指向包含节点较多的树根。这个策略与按秩合并的策略类似,同样可以提升并查集的运行速度,而且省去了额外的 rank 数组。

这样的并查集具有一个略微不同的定义,即若 uset 的值是正数,则表示该元素的父节点(的索引);若是负数,则表示该元素是所在集合的代表(即树根),而且值的相反数即为集合中的元素个数。相应的代码如下所示,同样包含递归和非递归的 find 操作:(并不是很理解,似乎是比较两个集合的元素多少来选择将谁做代表)

复制代码
 1 const int MAXSIZE = 500;
 2 int uset[MAXSIZE];
 3  
 4 void makeSet(int size) {
 5     for(int i = 0;i < size;i++) uset[i] = -1;
 6 }
 7 int find(int x) {
 8     if (uset[x] < 0) return x;
 9     uset[x] = find(uset[x]);
10     return uset[x];
11 }
12 int find(int x) {
13     int p = x, t;
14     while (uset[p] >= 0) p = uset[p];
15     while (x != p) {
16         t = uset[x];
17         uset[x] = p;
18         x = t;
19     }
20     return x;
21 }
22 void unionSet(int x, int y) {
23     if ((x = find(x)) == (y = find(y))) return;
24     if (uset[x] < uset[y]) {
25         uset[x] += uset[y];
26         uset[y] = x;
27     } else {
28         uset[y] += uset[x];
29         uset[x] = y;
30     }
31 }
复制代码

 

3.查找

查找操作如果不做路径压缩,会极大提高时间复杂度,尤其在数据很大时会容易超时。

路径压缩是在查找操作时顺便将每个集合中的元素连接到父节点(直接将祖宗做父亲),以方便查找操作,时间复杂度o(1)。

没有路径压缩:

1 int fa[MAXN];  // 记录某个人的爸爸是谁,特别规定,祖先的爸爸是他自己
2 int find(int x) {
3   // 寻找x的祖先
4   if (fa[x] == x)  // 如果x是祖先则返回
5     return x;
6   else
7     return find(fa[x]);  // 如果不是则x的爸爸问x的爷爷
8 }

路径压缩:

int find(int x){
    
    if(fa[x] == x) return x;
    else return fa[x] = find(fa[x]);//路径压缩 
    
}

例题:

1. 亲戚问题(最基本并查集问题)

2.广播系统(月考核)

题目描述

为了更加快速的传递学习任务,ACM集训队计划建设一个广播系统!按照规划,这个系统包含若干端点,这些端点由神奇的网络连接。
此网络有下述特点:
1.消息可以在任何一个端点产生,并且只能通过这个网络传递信息。每个端点接收消息后会将消息传送到与其相连的端点(单项传输,不会传输到那个消息发送过来的端点)
2.如果某个端点是产生消息的端点,那么消息将被传送到与其相连的每一个端点。
3.当消息在某个端点生成后,其余各个端点均能接收到消息
4.任意一个消息可以被快速的传给所有端点
现给你这个广播系统的连接方案,你能判断此系统是否符合以上要求并且传递给所有的端点?

Input

输入包含多组测试数据。每两组输入数据之间由空行分隔。
每组输入首先包含2个整数N和M,N(1<=N<=1000)表示端点个数,M(0<=M<=N*(N-1)/2)表示通信线路个数。
接下来M行每行输入2个整数A和B(1<=A,B<=N),表示端点A和B由神奇的网络相连。两个端点之间至多由一条网络直接相连,并且没有将某个端点与其自己相连的网络。
当N和M都为0时,输入结束。

Output

对于每组输入,如果所给的系统描述符合题目要求,则输出Yes,否则输出No。

sample Input

4 3
1 2
2 3
3 4

3 1
2 3

0 0

sample Output

Yes
No

这道题最重要的点是判断所给关系能否让所有元素成为一个联通块,成为一个集合且不能让集合成环。

我的思路是  在合并时如果两个元素已经属于一个集合就不再连接(避免成环)

并且初始化sum为1,每次在集合中增加一个元素就增加1,最后判断sum和n值是否相等;

代码:

 

 

部分内容摘自

出处:http://www.cnblogs.com/cyjb/
GitHub:https://github.com/CYJB/

posted @   倩影一梦  阅读(142)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示