并查集算法讲解+例题

定义:并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。


考虑这样一个问题:

       小明马上要过生日,要邀请N个好友,而他的好友都只愿意和认识的人坐,给出朋友之间的认识关系,假设所有认识的人坐一桌,问最少需要多少桌子。

       规定:若A认识B,B认识C,则A认识C,则ABC全部认识,可以坐一桌;

       例如:小明的好友为 A B C D E 五人,关系式 A-B B-C D-E。则此时需要两个桌子:ABC一桌,DE一桌。

       输入N表示好友人数,M表示关系数,接下来M行显示所有关系。

       样例输入:

       

9 7
2 4
5 7
1 3
8 9
1 2
5 6
2 3

问题分析:

如果对于所有的朋友编号1~N

不难想到,用集合来表示朋友之间的关系,就样例而言,最初没有给出任何关系的时候,9个人互相都不认识,于是有9个集合:

{1},{2},{3},{4},{5},{6},{7},{8},{9}

当依次输入关系之后,就可以依次合并集合:

输入:2 4 :{1},{2,4},{3},{5},{6},{7},{8},{9}

输入:5 7:{1},{2,4},{3},{5,7},{6},{8},{9}

输入:1 3:{1,3},{2,4},{5,7},{6},{8},{9}

输入:8 9:{1,3},{2,4},{5,7},{6},{8,9}

输入:1 2:{1,2,3,4},{5,7},{6},{8,9}

输入:5 6:{1,2,3,4},{5,6,7},{8,9}

输入:2 3:{1,2,3,4},{5,6,7},{8,9}


最后有3个集合,也就是所有的朋友分为3群,也就是说,需要3张桌子,最后的答案是3.

很简单?是的,让人来合并集合确实很简单,但是如何让计算机去合并集合呢?

你当然可以暴力的合并,但是超时那是妥妥的,于是伟大的并查集算法诞生了!

没错,这就是典型的并查集问题,对此类问题我们先画一下上面的关系:


并查集算法讲解+例题_第1张图片


只是将之前的集合关系画成图,还画的那么丑,并没有什么用啊?


我们开这样一个数组father[N];

其中,father[i] = j表示,i这个点的父结点是j

对应上面第3张图,也就是:

father[3] = 1,father[1] = 4,father[4] = 2,father[2] = 2;//想想这里为什么让father[2] = 2

father[7] = 5,father[5] = 6,father[6] = 6;

father[9] = 8,father[8] = 8;

仔细观察上面的关系,例如:father[ father[1] ] = 2;体现了1的父亲的父亲是2。然后你还会发现,上面3排的最后一个关系可以写成father[i] = i。这样的形式告诉我们,i已经是根节点了,没有必要再继续往上面找了。


这个图你懂了,但是这又有什么用呢?

不,你突然发现,用集合表示出来的关系已经可以体现出来了!

{1,2,3,4},{5,6,7},{8,9},这3个集合,以及第三张图......

似的,你发现,在图三中被一根线连起来的一坨就是一个集合,而那一坨的所有元素共有一个根节点


于是,问题迎刃而解,你只需要把“线”连起来,然后最后找一找根节点的个数就好,然后你又看出father[i] = i就是根节点所具有的性质。


于是我们便可以开始并查集算法了:

首先,不管是集合还是图,最初的初始情况是谁也不认识谁,每个点都是单独的个体,你也可以理解成每个点都是根节点,于是我们这样初始化:

初始化代码如下:

void init()
{
    for (int i = 0;i <= n;i++)
    {
        father[i] = i;
    }
}

找根节点:

int getfather(int x)
{
    while (x != father[x])
    {
        x = father[x];
    }
    return x;
}

这个地方有人可能会说:你这个循环不就相当于一个if 吗?x!=father[x]就让x=father[x]嘛,然后就可以跳出循环了嘛!

似乎很有道理,但是你就刚刚的“father[3] = 1,father[1] = 4,father[4] = 2,father[2] = 2”来模拟一下,

假设传入的x是3,x!=father[x],于是x = father[x],此时的 x = 1,再带入while循环的判断框,咦,原来这个时候的x=1,father[x] = 4依旧不等,然后你明白了其中的奥妙。

(可能你是一眼看出,但介于本渣当初学并查集的时候看了好久才明白,这里还是提一下,希望对于新手的理解更有帮助)


合并:

void Merge(int a,int b)
{
    int fa = getfather(a);
    int fb = getfather(b);
    if (fa != fb)
    {
        father[fa] = fb;
    }
}


基本上掌握上面3个模板,并查集就算入门了,那么直接上一道模板题练练手

题目传送门:点击打开链接

上AC代码:

//Must so
#include<bits/stdc++.h>
#define mem(a,x) memset(a,x,sizeof(a))
#define sqrt(n) sqrt((double)n)
#define pow(a,b) pow((double)a,(int)b)
#define inf 1<<29
#define NN 1006
using namespace std;
const double PI = acos(-1.0);
typedef long long LL;
int n,m;
int father[NN];
void init()
{
    for (int i = 0; i <= n; i++)
    {
        father[i] = i;
    }
}
int getfather(int x)
{
    while (x != father[x])
    {
        x = father[x];
    }
    return x;
}
void Merge(int a,int b)
{
    int fa = getfather(a);
    int fb = getfather(b);
    if (fa != fb)
    {
        father[fa] = fb;
    }
}
int main()
{
    int T;
    cin>>T;
    while (T--)
    {
        cin>>n>>m;
        init();
        for (int i = 0,a,b; i < m; i++)
        {
            scanf("%d%d",&a,&b);
            Merge(a,b);
        }
        int ans = 0;
        for (int i = 1; i <= n; i++)
        {
            if (father[i] == i) ans ++;
        }
        cout<<ans<<endl;
    }
    return 0;
}


做完这道简单题,于是你开是在并查集的路上越走越远,然后有一天,你发现你的并查集超时了!!!

于是,你开始知道一种名为“路径压缩”的优化方式:

什么叫“路径压缩”呢?其实就是一张图!

假设我最初给出的关系画成图是这样:

并查集算法讲解+例题_第2张图片

在这个图的基础上,你可能需要多次去找4的根节点,如果每次找都要通过4-3-2-1的路线去找就太傻了,于是有了下面这张图:

并查集算法讲解+例题_第3张图片

在每一次找到一个点的根节点之后,直接把这个点连到根节点上去,这样以后再找就会省下很多时间!

知道这个原理代码再修改起来也很简单,只需要在找爸爸的地方记录一下就好了

int getfather(int x)
{
    int xx = x;//保存这个需要找爸爸的点
    while (x != father[x])
    {
        x = father[x];
    }
    father[xx] = x;//直接将这个点连到根节点上去
    return father[xx];
}

然后同样是之前的那个题目可以试着用路径压缩做的试试,对比一下运行时间。

你可能感兴趣的:(算法,并查集)