跳转至

并查集 基础篇

概述

👉并查集 提高篇

定义: 并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。

主要构成: 并查集主要由一个整型数组pre[ ]和两个函数find( )、join( )构成。 数组 pre[ ] 记录了每个点的前驱节点是谁,函数 find(x) 用于查找指定节点 x 属于哪个集合,函数 join(x,y) 用于合并两个节点 x 和 y 。

find函数

1
2
3
4
5
6
int find(int x)                 
{
    while(pre[x] != x)          //如果x的上级不是自己
        x = pre[x];             //x继续找他的上级
    return x;                   //找到
}

join函数

1
2
3
4
5
6
void join(int x,int y)                     
{
    int fx=find(x), fy=find(y);   // 找x的上级,y的上级   
    if(fx != fy)                  // 上级不相等,合并         
        pre[fx]=fy;                       
}

路径压缩

从find函数可以看出,从某个节点出发寻找根节点时,需要途径一系列的节点,而这些节点的上级都是同一个,因此可以在寻找的过程中将这些节点的上级都顺带修改了,简单说来就是将x到根节点路径上的所有点的pre(上级)都设为根节点。代码如下:

1
2
3
4
5
int find(int x)                     //查找结点 x的根结点 
{
    if(pre[x] == x) return x;       //递归出口:x的上级为 x本身,即 x为根结点        
    return pre[x] = find(pre[x]);   // 修改途径节点的根节点
}

例1.合并集合

题目链接

题目描述

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m 个操作,操作共有两种:

  • M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  • Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;

输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a bQ a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

\(1≤n,m≤105\)

输入样例

1
2
3
4
5
6
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4

输出样例

1
2
3
Yes
No
Yes

题解

并查集模板题

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>

using namespace std;

const int N = 100010 ;

int p[N] , n , m;

int find(int x)
{
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}
int main()
{
    cin >> n >> m ;
    for(int i = 0 ; i <= n ; i ++) p[i] = i ;
    while( m --)
    {
        char op;
        int a, b ;
        cin >>op >> a>> b;
        if(op =='M') p[find(a)] = find(b);
        else
        {
            if(find(a) == find(b)) cout <<"Yes"<<endl;
            else cout<<"No"<<endl;
        }
    }


}

例2.连通块中点的数量

题目链接

题目描述

给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。

现在要进行 m 个操作,操作共有三种:

  • C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;
  • Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;
  • Q2 a,询问点 a 所在连通块中点的数量;

输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 C a bQ1 a bQ2 a 中的一种。

输出格式

对于每个询问指令 Q1 a b,如果 a 和 b 在同一个连通块中,则输出 Yes,否则输出 No

对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量

每个结果占一行。

数据范围

\(1≤n,m≤105\)

输入样例

1
2
3
4
5
6
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5

输出样例

1
2
3
Yes
2
3

题解

C和Q1操作为并查集基本操作,Q2操作要求知道与a有相同根节点的节点所构成集合的大小,因此在每次join操作时更新集合大小即可

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<iostream>

using namespace std;
const int N = 100010;
int n , m ;
int p[N] ,psize[N]; // psize存集合大小

int find(int x )
{
    if(p[x] != x ) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    cin >> n >> m;
    for(int i = 1 ; i <= n ; i ++)
    {
        p[i] = i ;
        psize[i] = 1;
    }
    while(m --)
    {
        string op;
        int a , b;
        cin>>op;
        if(op =="C")
        {
            cin>>a>>b;
            if(find(a) == find(b)) continue;
            psize[find(b)] +=psize[find(a)];
            p[find(a)] =find(b);
        }
        else if(op == "Q1")
        {
            cin >> a >> b;
            if(find(a) == find(b)) cout << "Yes"<<endl;
            else cout<< "No"<<endl;
        }
        else
        {
            cin>>a;
            cout<<psize[find(a)]<<endl;
        }

    }
    return 0;
}

例3.食物链

题目链接

题目描述

动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。

A 吃 B,B 吃 C,C 吃 A。

现有 N 个动物,以 1∼N 编号。

每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N 个动物所构成的食物链关系进行描述:

第一种说法是 1 X Y,表示 X 和 Y 是同类。

第二种说法是 2 X Y,表示 X 吃 Y。

此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  • 当前的话与前面的某些真的话冲突,就是假话;
  • 当前的话中 X 或 Y 比 N 大,就是假话;
  • 当前的话表示 X 吃 X,就是假话。

你的任务是根据给定的 N 和 K 句话,输出假话的总数。

输入格式

第一行是两个整数 N 和 K,以一个空格分隔。

以下 K 行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中 D 表示说法的种类。

若 D=1,则表示 X 和 Y 是同类。

若 D=2,则表示 X 吃 Y。

输出格式

只有一个整数,表示假话的数目。

数据范围

\(1≤N≤50000,\)

\(0≤K≤100000\)

输入样例

1
2
3
4
5
6
7
8
100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5

输出样例

1
3

题解

解法1:若x和y已经确定关系,则x和y已经有了相同的根节点,因此我们可以通过维护x和y到根节点的距离来判断他们之间的关系,对3取余后距离取值为0,1,2,分别对应x和y同类x吃yy吃x三种状态,若根节点相同,判断距离,若不相同,则合并即可

解法2:扩展域并查集,开三倍空间,令1-n为同类域,n+1-2n为捕食域,2n+1-3n为天敌域,每次修改时同时维护三个域,例如x吃y,则在x的捕食域中加入y,在y的天敌域中加入x,同时在y的捕食域是捕食x的天敌域。

代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// 解法1

#include <iostream>

using namespace std;

const int N = 50010;

int n, m;
int p[N], d[N];

int find(int x)
{
    if (p[x] != x)
    {
        int t = find(p[x]);
        d[x] += d[p[x]];
        p[x] = t;
    }
    return p[x];
}

int main()
{
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; i ++ ) p[i] = i;

    int res = 0;
    while (m -- )
    {
        int t, x, y;
        scanf("%d%d%d", &t, &x, &y);

        if (x > n || y > n) res ++ ;
        else
        {
            int px = find(x), py = find(y);
            if (t == 1)
            {
                if (px == py && (d[x] - d[y]) % 3) res ++ ;
                else if (px != py)
                {
                    p[px] = py;
                    d[px] = d[y] - d[x];
                }
            }
            else
            {
                if (px == py && (d[x] - d[y] - 1) % 3) res ++ ;
                else if (px != py)
                {
                    p[px] = py;
                    d[px] = d[y] + 1 - d[x];
                }
            }
        }
    }

    printf("%d\n", res);

    return 0;
}

// 解法2

//这里我们将三个域,分别转化为了n,n+n,n+n+n.因为读入方面特别烦.
#include <bits/stdc++.h>
using namespace std;
int fa[200000];
int n,m,k,x,y,ans;
int get(int x)
{
    if(x==fa[x]) 
        return x;
    return fa[x]=get(fa[x]);
}
void merge(int x,int y)
{
    fa[get(x)]=get(y);
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=3*n;i++) 
        fa[i]=i;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&k,&x,&y);
        if(x>n || y>n) 
            ans++;
        else if(k==1)
        {
            if(get(x)==get(y+n) || get(x)==get(y+n+n)) //如果x,y是同类,但是x是y的捕食中的动物,或者x是y天敌中的动物,那么错误.
                ans++;
            else
            {
                merge(x,y);
                merge(x+n,y+n);
                merge(x+n+n,y+n+n);
            }
        }
        else
        {
            if(x==y || get(x)==get(y) || get(x)==get(y+n)) //x就是y,或者他们是同类,再或者是y的同类中有x
                ans++;//都是假话
            else
            {
                merge(x,y+n+n);//y的天敌域加入x
                merge(x+n,y);//x的捕食域加入y
                merge(x+n+n,y+n);//x的天敌域是y的捕食域.
            }
        }
    }
    cout<<ans<<endl;
}
//x是同类域.
//x+n是捕食域
//x+n+n是天敌域