并查集

Posted ColdCode

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并查集相关的知识,希望对你有一定的参考价值。

一、定义

并查集是一种树形的数据结构,用于处理一些不相交集合的合并以及查询问题。

二、操作

1、void make_set(int n)

含义:有n个元素,把这n个元素初始化成n个集合,每个集合包含1个元素。

2、int find_root(int x)

含义:查找元素x所在的集合,返回集合的根结点。通过查找两个元素的根结点是否相同,可以判断两个元素是否属于同一个集合。

3、void union_set(int a, int b)

含义:合并两个不相交的集合。

三、举例

假设有3个元素,编号分别为1,2,3,则进行初始化make_set后,这3个元素形成了3个集合{1},{2},{3},集合之间互不相交,对应的树如下图所示:

可以看到3个元素形成了3颗独立的树,每颗树只包含一个根结点。我们使用数组p[x]来表示结点x的父结点,若x为根结点,则令p[x]=-1(或者令p[x]=x),所以有p[1]=-1,p[2]=-1,p[3]=-1。合并集合则是将一棵树的根结点变成另一颗树的孩子结点,合并集合{1},{2}后,对应的数如下所示:

此时有p[1]=-1,p[2]=1,p[3]=-1。再将集合{3}与集合{1,2}合并,则有

此时p[1]=-1,普p[2]=1,p[3]=1,3个集合最终合并成了一个集合。

四、优化

并查集最初的状态时一个个不相交的集合,如果只是简单的合并而不采取任何优化,那么树高可能会不断增加,最坏的情况是一棵树退化成了一条链,树高越高,查找根结点所花费的时间也就越长,我们希望树应该尽可能的低(两层),可以使用下面两种方法来进行优化:

1、路径压缩

在查找某结点x的根结点的过程中,将结点x与根结点和x之间的所有结点都直接指向根结点,这就是路径压缩。如下图:

代码实现(递归):

//查找根结点时路径压缩,递归实现
int find_root(int x)
{
    if(p[x] == -1)
        return x;
    else 
    {
        int t = find_root(p[x]);
        p[x] = t;
        return t;
    }
}

代码实现(非递归):

//查找根结点时路径压缩,非递归实现
int find(int x)
{
    int k, j, r;
    r = x;
    while(r != p[r])     //查找跟节点
        r = p[r];      //找到跟节点,用r记录下
    k = x;        
    while(k != r)             //非递归路径压缩操作
    {
        j = [k];         //用j暂存parent[k]的父节点
        p[k] = r;        //parent[x]指向跟节点
        k = j;                    //k移到父节点
    }
    return r;         //返回根节点的值            
}

2、按秩合并

按秩合并即将元素少的集合合并到元素多的集合中,这样有利于降低树高。

五、代码实现

假设有n个元素,编号为1~n,现在输入这n个元素中的m个元素,输出最终建立的树中每个结点的父结点。

 1 #include <iostream>
 2 #include <cstdio>
 3 #include <vector>
 4 using namespace std;
 5 
 6 const int N = 1000;
 7 int p[N];
 8 vector<int> v;  //存储输入的元素编号
 9 
10 int make_set(int n)
11 {
12     for(int i=1; i<=n; i++)
13         p[i] = -1;  //初始化,每个结点为一棵树
14 }
15 
16 int find_root(int x)
17 {
18     if(p[x] == -1)
19         return x;
20     else
21     {
22         int t = find_root(p[x]);    //路径压缩
23         p[x] = t;
24         return t;
25     }
26 }
27 
28 void union_set(int a, int b)
29 {
30     int ra = find_root(a);
31     int rb = find_root(b);
32 
33     if(ra != rb)    //a,b不在同一集合中,合并
34         p[ra] = rb;
35 }
36 
37 int main()
38 {
39     int n, m;
40     scanf("%d%d",&n,&m);
41     make_set(n);
42     int a, b;
43     scanf("%d", &a);
44     v.push_back(a);
45     for(int i=1;i<m;i++)
46     {
47         scanf("%d", &b);
48         v.push_back(b);
49         union_set(a, b);
50         a = b;
51     }
52 
53     for(int i=0; i<v.size(); i++)
54         printf("p[%d]=%d\\n", v[i], p[v[i]]);
55     return 0;
56 }

输入:

3 3
1 2 3

输出:

p[1]=2
p[2]=3
p[3]=-1

六、带权并查集

普通并查集只包含了结点与结点之间是否属于同一集合的信息,而带权并查集则加入了一个数组r[x],记录了结点x与其父结点之间的信息(通常情况下),比如是否是同一类,有了多余的信息r[x]相当于结点x有了权值,这也是“带权”的含义。在进行路径压缩find_set和合并union_set时,r[]也要进行相应的变化,按秩合并就是带权并查集的一个应用。

七、例题

普通并查集:poj1611poj2524hdoj1232

带权并查集:poj2492poj1703poj1182

以上是关于并查集的主要内容,如果未能解决你的问题,请参考以下文章

想要学会并查集吗?看我四十行代码实现它

树--12---并查集

笔记并查集---无向图处理代码模板及类型题

并查集

力扣 每日一题 886. 可能的二分法难度:中等,rating: 1794(并查集 / 拆点优化的扩展域并查集)

并查集