简单易懂的并查集算法以及并查集实战演练

前言

并查集算法适用于处理一些不相交集合的合并及查询问题。对于这一类的问题使用并查集,不但节省了空间,而且大大缩短了运行时间。
基本的并查集很好写出一个模板,对于一些特殊的题目也能很好对并查集进行变形,接下来来看一下引例了解一下并查集


一、引例

男生寝室关系错综复杂,甚至一个四人寝室都能整出四个爸爸四个儿子。
有一天宿管阿姨想知道这个寝室楼里有多少个爸爸家族,但是宿管只知道哪两个人互称爸爸。


二元关系图

上图是宿管阿姨花了三天三夜整理的爸爸关系图,宿舍楼里总共有n个男生,所以她将每个男生编号0~(n-1)。刚开始人不多,所以阿姨想看0和6是不是一个家族的,只需要看这几个关系即可知道0和6是一个爸爸家族的


查找0与6的关系

后来学生知道了,纷纷来宿管这儿查关系,想看看自己有多少个儿子,自己和隔壁班的学霸是不是爸爸关系。
这一下可给宿管忙坏了,有时候得追溯到十几个人才能确定两个人有爸爸关系,而且还会查错走不少弯路,于是阿姨花了一个晚上,硬生生把关系无向图给画出来了,这下子学生一看图就能知道自己在爸爸家族有多少儿子。
树状图爸爸家族

时间一长,学生跑来和宿管讲:“阿姨阿姨,我又有新儿子了,我是0,我儿子是7”,于是阿姨在图上简单地画一下,一个新关系图出来了


0与7合并

二、结合引例写出并查集

1. 并查集维护一个数组

说来你可能不信,以上就是并查集的思路了!下面我们回归正题,在上面例子中,我们涉及到了两个操作,一个是查找,一个是合并,这也是并查集名字的由来。

在这里我们新建一个数组arr,图示第一行为坐标i,第二行为数组元素的内容arr[i],数组坐标就是学生的编号,分别是0, 1, 2, ..., n-1。arr[x]的值表示编号为x的同学的爸爸是谁。初始值表示自己就是自己的爸爸,所以arr[x]=x。
[图片上传失败...(image-f0cb39-1612082280193)]

2. 并查集的 并 操作

接下来一个“父子关系”加入了,0说3是儿子,3说0是儿子,谁也争持不下,那就偷偷的规定右边是左边的爸爸,不告诉他们。(你可能会疑惑,为什么要这样规定,可以反过来吗?后面会做出解释,你姑且先放一边哈)
[图片上传失败...(image-8eb306-1612082280193)]
接下来循环往复,重复以上操作,依次将2与5,4与6合并
[图片上传失败...(image-472d9e-1612082280193)]
我们通过对数组元素的修改,得到了一个集合,我们可以用无向图来表示。按照0的爸爸是3,3的爸爸是4,4的爸爸是6等这样来把它们连起来


合并后的无向图

3. 并查集的 查 操作

我们要查找0和4是不是一个家族的,就需要查看0的祖先和4的祖先是同一个人吗?换句话说也就是只需要向上依次查询0的爸爸的爸爸的爸爸,查询4的爸爸的爸爸的爸爸,直到最后的爸爸的爸爸是他自己,就说明这个是爸爸家族里的祖先。下面给出图示说明
[图片上传失败...(image-80b71c-1612082280193)]
做出一点解释,对于第一步,因为数组arr对应的arr[x]=y,表示编号为x的人的爸爸是y,所以要想知道x的父亲,必须访问arr[x]即可知道
对于第二步,祖先一定满足arr[x]=x,即爸爸的爸爸是自己,所以只要不满足arr[x]=x的,一定x一定还有爷爷,这时候就要一直向上查询x的爷爷爷爷爷爷爷爷,直到第六步所示,arr[6]=6表示6是祖先!我们找到了!!

4. 基本并查集模板代码实现——第一版(有错误后面分析)

这样一来,我们简单地得到了第一版的并查集:

/**
 * @author 太白
 */
public class UnionFind {
    private int[] arr;
    public UnionFind(int size) {
        arr = new int[size];
        for (int i = 0; i < size; i++) {
            arr[i] = i;     // 初始化,每个人是自己的父亲,即每个人都是祖先
        }
    }

    public int find(int son) {
        int father = arr[son];
        // 循环,直到爸爸的爸爸是自己为止
        while (father != arr[father]) {
            father = arr[father];
        }
        return father;
    }
    
    public void union(int son1, int son2) {
        // 此处有错误,可以思考一下,下面我们举例说明为什么错了
        arr[son1] = son2;   // 合并,son1的爸爸是son2
    }
}

5. 一个错误

我们给出一种情况,我们加了一个新关系,0的爸爸是2,来看看我们现在的代码是否能完成预期功能

添加0与2的关系

完蛋!由于我们在union方法中只是进行arr[son1]=son2;导致原先的祖爷爷6少了一个孙子0,孙子0归祖爷爷5去了,简直是背盟败约,跑到了别的爸爸家族里面去了!!!
没事,其实解决方法特别简单!别被这个小错误给乱了阵脚,这也是起初学该算法容易犯的错误,可以注意一下。
我们维护的是m个爸爸家族,所以如果我们将整个爸爸家族当做是一个人,另一个爸爸家族当做是另一个人,再让这两个巨大的人互认爸爸儿子即可。自然,我们肯定会让祖爷爷出面,去和另一个家族比拼,谁赢了就谁当爸爸。(当然无所谓谁当,后面会解释)
正确的插入关系

6. 基本并查集模板代码实现——第二版(解决错误)

现在,我们给出第二版,解决了上述的BUG,你会看到解决方式是如此的简单,代码也很好理解,找到双方的祖先,最后让son2的祖先当son1祖先的爸爸。

/**
 * @author 太白
 */
public class UnionFind {
    private int[] arr;
    public UnionFind(int size) {
        arr = new int[size];
        for (int i = 0; i < size; i++) {
            arr[i] = i;
        }
    }

    public int find(int son) {
        int father = arr[son];
        while (father != arr[father]) {
            father = arr[father];
        }
        return father;
    }
    
    public void union(int son1, int son2) {
        // 请相信我,只改动了这里,其他地方都没有修改过
        int father1 = find(son1);
        int father2 = find(son2);
        // 注意中括号里面的son改成了father
        arr[father1] = father2;
    }
}

7. 一点点解释,为什么无需在意谁是谁的爸爸

在上面我们的代码中默认了都是son1的爸爸是son2,那我们可以让son2的爸爸是son1吗?答案是可以!
因为我们维护的是集合,只是确认一个集合里有谁即可。
从语义上讲:今天你是我爸爸,明天我是你爸爸,但是我们依然是一个爸爸家族里的一员,所以不需要在意谁是谁的爸爸。
从家族结构来讲:整个家族就是一个无向图,关系是双向的。0可以是3的爸爸,3也可以是0的爸爸,所以无需刻意要求,当然也可以在代码里反着写arr[father2]=father1


三、并查集的优化

1. 并查集优化原理

可以看到我们每次查询都得一个一个向上查,0的爸爸是3,3的爸爸是4,4的爸爸是6,6的爸爸是6...如果我们数据量大一点,一个宿舍楼上万人,那么我们每次查询最大可能得查上万次,这太花时间啦!那么我们能不能做出一点改进呢?诶你别说,还真有~
思路是这样的,毕竟我们每次都得查询x的祖先是谁,不关心x的爸爸是谁,x的爷爷是谁,那为什么不直接让arr[x]指向它的祖先编号y呢,在本例中我们为什么不让arr[0]=6, arr[3]=6, arr[4]=6呢?
所以我们在本例中,可以让0、3、4的爸爸直接认定为6即可。延续上一次的查找,我们再多一项功能就是再次遍历0的爸爸3、4,让3和4的爸爸设置为6

[图片上传失败...(image-65f0e2-1612082280193)]

2. 基本并查集模板代码实现——第三版(优化)

代码实现也很简单,我们只需要在find方法中加个四五行即可

/**
 * @author 太白
 */
public class UnionFind {
    private int[] arr;
    public UnionFind(int size) {
        arr = new int[size];
        for (int i = 0; i < size; i++) {
            arr[i] = i;
        }
    }

    public int find(int son) {
        int father = arr[son];
        while (father != arr[father]) {
            father = arr[father];
        }
        // 直到循环到祖先为止
        while (father != arr[son]) {
            // 保存当前son的爸爸
            // 因为要更改son的爸爸,所以要有个备份
            int next = arr[son];
            // 将son的爸爸改成father
            arr[son] = father;
            // 找到下一个爸爸(用备份的爸爸赋值给son即可)
            // 将son改成下一个爸爸,为下一个循环做准备
            son = next;
        }
        return father;
    }

    public void union(int son1, int son2) {
        int father1 = find(son1);
        int father2 = find(son2);
        arr[father1] = father2;
    }
}

于是,我们的模板就算是做好了,我习惯在里面添加两个方法,一个是isFamily(int, int),另一个是getCategory(),第一个可以检测两个人是不是同一个爸爸家族的成员,另外一个看看总共有多少个爸爸家族,这两个方法经常在题目中用到,也很好实现,所以后续我们会结合例题,看如何十分简单地运用该算法解决一些相对复杂的题目。


四、例题

未完待续...

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,820评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,648评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,324评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,714评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,724评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,328评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,897评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,804评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,345评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,431评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,561评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,238评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,928评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,417评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,528评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,983评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,573评论 2 359

推荐阅读更多精彩内容

  • 并查集的思想 并查集是一种树形的数据结构,用于处理不相交集的合并查询,一般具有两个基本的操作,查找确定元素在哪一个...
    Tim在路上阅读 1,438评论 0 2
  • 又来记录学习了鸭~好记性不如烂笔头 记录除了可以更好地学习 其次就是可以发现自己之前的理解是否出错了! 首先,Ap...
    LiBiscuit阅读 22,680评论 2 14
  • 课程介绍 先修课:概率统计,程序设计实习,集合论与图论 后续课:算法分析与设计,编译原理,操作系统,数据库概论,人...
    ShellyWhen阅读 2,299评论 0 3
  • 并查集被很多OIer认为是最简洁而优雅的数据结构之一,主要用于解决一些元素分组的问题。它管理一系列不相交的集合,并...
    Pecco阅读 364评论 0 0
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,524评论 16 22