算法学习 —— 并查集

(洛谷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个询问的答案为“具有”或“不具有”亲戚关系。

要解决这样一种多个亲戚的关系,我们可以采用并查集的方法。那么什么是并查集呢?定义如下:

并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。

顾名思义,并查集支持两种操作:

合并(Union):合并两个元素所属集合(合并对应的树)
查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合

只看概念很难理解这到底是什么东西。我们将用通俗易懂的语言来解释什么是并查集。
首先在一个集合中将有关的元素连接起来形成树的结构,再遇到相关元素时,将新元素插入到树中,这样在查询A与B的相关性时我们可以通过对A祖宗节点的查询判断相关性(都连接在一起了)。
原本是
在这里插入图片描述
现在是
在这里插入图片描述
由此我们可以写出并查集的代码。
初始化

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

其中fa数组用来存储对应角标元素的爹
初始化时自己是自己的爹

查询
查询时我们需要在一次没找到对应关系后进行递归查询,万一是哪位祖宗呢?

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

合并
将两个树的根节点一个附着在另一个上当儿子就行

inline void merge(int i, int j)
{
    fa[find(i)] = find(j);
}

路径压缩

随着加入的节点越来越多,我们会发现,这个树会变成这样:
在这里插入图片描述
很明显,这样是很不便与我们找寻根节点的。
因此,我们需要将这棵树变成这样:
在这里插入图片描述
将节点直接与根节点相连接,就能在查找时变得更快。

我们该怎么实现呢?
很简单,我们只需在查找父节点时顺手将该元素目前的父节点更改为递归的值,就行了。

int find(int x)
{
    if(x == fa[x])
        return x;
    else{
        fa[x] = find(fa[x]);  //父节点设为根节点
        return fa[x];         //返回父节点
    }
}

是不是很简单?使用三问表达式我们还能更简单:

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

这样我们就可以得到结果了。

按秩合并

当我们遇到两颗树时,我们该把哪一颗树保留,让另一颗树往上补呢?
在这里插入图片描述
如这种情况下,是8做7的儿子还是7作8的儿子呢?当然是将8向7上靠。因为这样我们得出的树的最终深度将会最小,我们查询所耗费的时间将会是最小。同理,我们在合并两颗树的时候,应当按照深度来决定谁作父节点。这就是按秩合并的含义。

在代码实现中,我们引入一个新的数组rank来记录树的深度。
初始化

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

合并

inline void merge(int i, int j)
{
    int x = find(i), y = find(j);    //先找到两个根节点
    if (rank[x] <= rank[y])
        fa[x] = y;
    else
        fa[y] = x;
    if (rank[x] == rank[y] && x != y)
        rank[y]++;                   //如果深度相同且根节点不同,则新的根节点的深度+1
}

为什么+1?
在这里插入图片描述
合并完
在这里插入图片描述
显然+1

例题代码:

#include <cstdio>
#define MAXN 5005
int fa[MAXN], rank[MAXN];
inline void init(int n)
{
    for (int i = 1; i <= n; ++i)
    {
        fa[i] = i;
        rank[i] = 1;
    }
}
int find(int x)
{
    return x == fa[x] ? x : (fa[x] = find(fa[x]));
}
inline void merge(int i, int j)
{
    int x = find(i), y = find(j);
    if (rank[x] <= rank[y])
        fa[x] = y;
    else
        fa[y] = x;
    if (rank[x] == rank[y] && x != y)
        rank[y]++;
}
int main()
{
    int n, m, p, x, y;
    scanf("%d%d%d", &n, &m, &p);
    init(n);
    for (int i = 0; i < m; ++i)
    {
        scanf("%d%d", &x, &y);
        merge(x, y);
    }
    for (int i = 0; i < p; ++i)
    {
        scanf("%d%d", &x, &y);
        printf("%s\n", find(x) == find(y) ? "Yes" : "No");
    }
    return 0;
}
  • 22
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
并查集是一种常用的数据结构,用来解决集合的合并和查找问题。它具有快速的合并和查找操作,且在某些场景下比其他数据结构更为高效。 并查集包含以下几个关键步骤: 1. 初始化:首先将每个元素作为一个单独的集合,每个集合代表一个独立的元素。 2. 查找操作:通过查找操作可以快速找到某个元素所属的集合。这里使用了路径压缩的优化方法,即在查找的同时将当前元素直接指向根节点,以加速以后的查找操作。 3. 合并操作:当需要将两个元素所属的集合合并时,可以通过合并操作将一个元素的根节点指向另一个元素的根节点。这样就能实现将两个集合合并成一个集合的目的,从而实现集合的合并操作。 并查集的核心思想是通过维护每个元素的根节点,来判断元素之间的关系。如果两个元素具有相同的根节点,说明它们属于同一个集合;如果两个元素具有不同的根节点,说明它们属于不同的集合。 并查集可以解决一些实际问题,例如判断无向图中的两个节点是否连通,以及朋友圈的数量等。在这些问题中,我们可以使用并查集来维护每个节点所属的集合,从而更高效地进行相关的操作。 总之,并查集是一种有效处理集合合并和查找问题的数据结构,具有简单、高效的特点。通过合理地应用,并查集能够在解决某些实际问题时,提供更高效的算法实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值