2021.11.28 并查集

并查集


一、关于并查集

1.1 并查集概念引入

并查集,顾名思义,就是合并查询集合,是一种特殊的树状数据结构。一般是用来解决元素分组的问题,或是管理一些不相交的集合,他支持两种操作:1.合并(Union)2.查询(Find),比较典型的问题就是找亲戚,最小生成树的Kruskal算法等等,我们可以考虑使用并查集进行维护。。

并查集的主要思想是用一个集合中的元素来代表这个集合。

这么说可能有点抽象,有人用武侠的例子来生动刻画了这个并查集,有兴趣可以了解一下 链接在这.

简单来说,就是有一堆不认识的人,他们需要拉帮结派,这里需要解决两个问题,第一,怎么让他们结盟(合并),第二,怎么知道他们是不是同一个帮派(查询)

1.2 合并

根据这几张图可以感受一下他们是怎么样建立关系的
并查集1.jpg
初始状态下大家都是分散的,各自成一派,所以自己的教主都是自己
并查集2.jpg
后面2和3想加入1的帮派,于是将他们的教主设为1,这样我就只要知道他们教主是谁,就知道他们是不是这帮派的人
并查集3.jpg
5 6和4同盟
并查集4.jpg
4又和1同盟了,那么大家全都是一家人,英文我们的教主都是1,咱们的头是一样的,那肯定是一帮子人。
并查集5.jpg
最后的树结构是这样的

另外提一下,这里的教主比较专业的叫法是生成元。我们并查集就是通过生成元来判断是不是隶属于这个集合的

1.3 查询

我们已经知道了他们怎么在同一个集合里的,不就是看他们的教主嘛,那我查他们是不是一路子人,就看他们教主是不是一样的就可以了。

那现在的主要问题就是怎么知道他的教主。

刚刚最后的图样很明显,是一个树形的数据结构,我们通过访问其上一级(引入pre数组访问),访问到最上面的一层以后肯定能找到教主的。所以我们很自然想到的方法就是递归

直接上代码。

二、代码实现

初始化(每个人就是一个帮派,自己的教主就是自己本身)

void init(int num)
{
    //一开始是盘散沙,每个人都是各立一派,
    //所以自立为王,他们的上一级(pre)就是他们自己
    for (int i = 1; i <= num; i++)
    {
        pre[i] = i;
    }
}

找生成元(递归思想)

int find(int key)
{
    if (pre[key] == key)
        return key;
    return find(pre[key]);
}

合并

void unite(int x, int y)
//并查集中的‘并’函数
//就是让x,y结成同盟(让x,y的老大都是一样的)
//即隶属于同一个集合内
{
    int root_x = find(x);
    int root_y = find(y);
    if (root_x == root_y)
        return;
    else{
        pre[root_x] = root_y;
    }
}

三、一些优化

3.1 查询路径压缩

我们希望我们只要找到最后的教主就好了,但是每一轮我都这样递归访问,明显会有点麻烦,具体如图。
并查集6.jpg

那有没有解决问题的办法呢?答案是有的,要解决递归引起的问题,我们就在递归上下点功夫,我们是否能将最后递归找到的教主,赋值给每一个之前递归过程中的元素呢?那我们多一个赋值操作就行了,后续我访问的时候,至少这些访问过的节点可以一步到位直接访问到教主,这就是那些大佬们总是说的路径压缩。

并查集7.jpg

int find(int key)
//并查集中的’查‘函数
//找教主,要是不是自己的话,就递归往上找
//这边是优化了一下算法,在递归过程中又赋值一下
//只要查过了一次老大后面就只要调用一次就能找到老大(root)了
{
    if (pre[key] == key)
        return key;
    return pre[key] = find(pre[key]);
}

厉害一点的可以直接一行三元运算符?: 的代码搞定

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

3.2 合并路径压缩(按秩合并)

那样优化以后,大家普遍会有一个认识误区,并查集最终生成的树深度是2,但是要注意路径压缩只在查询时进行,也只压缩一条路径,所以并查集最终的结构仍然可能是比较复杂的

在合并问题上,我们也可以留意一下,之前合并的操作的时候,或许你会想着一个问题,为什么要委屈x,让他的帮主上一级变成y的帮主,让y的帮主的上一级变成x的帮主不行吗?其实是可以的,合理的变,在路径上甚至还会有所优化。这是大佬们说的并查集按秩合并。

并查集8.jpg

观察这个图可以发现,树的深度变小了,这意味着访问教主所要递归的次数变少了。具体我们应该把深度小的树合并到深度大的树上去,这样合并以后,到根节点距离变长的节点个数会更少。

代码上需要做的改变是

这里我们引入一个Rank数组,记录每一个节点对应的深度
初始化的时候我们做一点改变,刚开始的深度是1

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

合并操作代码优化

void unite(int x, int y)
{
    int root_x = find(x);
    int root_y = find(y);
    //已经是同一个集合的话就不需要做了
    if (root_x == root_y)
        return;

    //小树并大树
    if (depth[root_x] > depth[root_y])

        pre[root_y] = root_x;
    else
    {
        if (depth[root_x] == depth[root_y])
            depth[y]++;
        pre[root_x] = root_y;
    }
}

这里只有一种情况是要改变树的深度的,那就是当两棵树的深度相同时,深度要加1。小数并大树的时候,深度还是大树的深度。不会变的

请看,这是两个树深度相同的情况
1

插入到另外一组树后,深度会+1,因为插入的树上面顶着被插树的根节点,所以深度多了一,倒过来插也是一样的。
2

四、题目试炼

2017年第八届 蓝桥杯C组 C/C++决赛 合根植物

题目描述

 w星球的一个种植园,被分成 m * n 个小格子(东西方向m行,南北方向n列)。每个格子里种了一株合根植物。
  这种植物有个特点,它的根可能会沿着南北或东西方向伸展,从而与另一个格子的植物合成为一体。

  如果我们告诉你哪些小格子间出现了连根现象,你能说出这个园中一共有多少株合根植物吗?
输入格式
  第一行,两个整数m,n,用空格分开,表示格子的行数、列数(1<m,n<1000)。
  接下来一行,一个整数k,表示下面还有k行数据(0<k<100000)
  接下来k行,第行两个整数a,b,表示编号为a的小格子和编号为b的小格子合根了。

  格子的编号一行一行,从上到下,从左到右编号。
  比如:5 * 4 的小格子,编号:
  1 2 3 4
  5 6 7 8
  9 10 11 12
  13 14 15 16
  17 18 19 20

样例输入

5 4
16
2 3
1 5
5 9
4 8
7 8
9 10
10 11
11 12
10 14
12 16
14 18
17 18
15 19
19 20
9 13
13 17

样例输出

5

其合根情况参考下图:
image.png

我的AC代码附上~

#include <stdio.h>

const int MAX_N = 1000005;
int pre[MAX_N], depth[MAX_N];
int m, n, x, y, T,sum;

void init(int num)
{
    //一开始是盘散沙,
    //每个人都是各立一派,所以自立为王,
    //他们的上一级(pre)就是他们自己
    for (int i = 1; i <= num; i++)
    {
        pre[i] = i;
        depth[i] = 0;
    }
}

int find(int key)
//并查集中的’查‘函数
//找教主,要是不是自己的话,就递归往上找,
//这边是优化了一下算法,在递归过程中又赋值一下,
//只要查过了一次老大后面就只要调用一次就能找到老大(root)了
{
    if (pre[key] == key)
        return key;
    return pre[key] = find(pre[key]);
}

void unite(int x, int y)
//并查集中的‘并’函数
//就是让x,y结成同盟(让x,y的老大都是一样的)
//即隶属于同一个集合内
{
    int root_x = find(x);
    int root_y = find(y);
    if (root_x == root_y)
        return;
    if (depth[root_x] > depth[root_y])
        pre[root_y] = root_x;
    else
    {
        if (depth[root_x] == depth[root_y])
            depth[y]++;
        pre[root_x] = root_y;
    }
}

int main()
{
    // freopen("1.in", "r", stdin);
    // freopen("2.out", "w", stdout);
    scanf("%d %d", &m, &n);
    scanf("%d", &T);
    init(m * n);
    while(T--){
        scanf("%d %d", &x, &y);
        unite(x, y);
    }
    for (int i = 1; i <= m * n; i++)
    {
        if (find(i) == i)
        {
            sum++;
        }
    }
    printf("%d\n", sum);
    return 0;
}

后续关注更新~~~~

posted @ 2021-11-28 16:55  yuezi2048  阅读(30)  评论(0编辑  收藏  举报