Union-Find 并查集算法详解

文章目录

  • Union-Find 并查集算法详解
  • 一、问题介绍
  • 二、基本思路
  • 三、平衡性优化
  • 四、路径压缩
  • 五、总结
  • 六、例题

一、问题介绍

简单说,动态连通性其实可以抽象成给一幅图连线。比如下面这幅图,总共有 10 个节点,他们互不相连,分别用 0~9 标记:

现在我们的 Union-Find 算法主要需要实现这两个 API:

class UF
{/* 将 p 和 q 连接 */public void union(int p, int q);/* 判断 p 和 q 是否连通 */public boolean connected(int p, int q);/* 返回图中有多少个连通分量 */public int count();
}

这里所说的 「连通」 是一种等价关系,也就是说具有如下三个性质:

  • 1、自反性:节点p和p是连通的。
  • 2、对称性:如果节点p和q连通,那么q和p也连通。
  • 3、传递性:如果节点p和q连通,q和r连通,那么p和r也连通。

比如说之前那幅图,0~9 任意两个不同的点都不连通,调用connected都会返回 false,连通分量为 10 个。

如果现在调用union(0, 1),那么 0 和 1 被连通,连通分量降为 9 个。

再调用union(1, 2),这时 0,1,2 都被连通,调用connected(0, 2)也会返回 true,连通分量变为 8 个。


判断这种「等价关系」非常实用,比如说编译器判断同一个变量的不同引用,比如社交网络中的朋友圈计算等等。

这样,你应该大概明白什么是动态连通性了,Union-Find 算法的关键就在于union和connected函数的效率。那么用什么模型来表示这幅图的连通状态呢?用什么数据结构来实现代码呢?

二、基本思路

注意我刚才把「模型」和具体的「数据结构」分开说,这么做是有原因的。因为我们使用森林(若干棵树)来表示图的动态连通性,用数组来具体实现这个森林

怎么用森林来表示连通性呢?我们设定树的每个节点有一个指针指向其父节点,如果是根节点的话,这个指针指向自己。

比如说刚才那幅 10 个节点的图,一开始的时候没有相互连通,就是这样:

class UF
{// 记录连通分量private int count;// 节点 x 的节点是 parent[x]private int[] parent;/* 构造函数,n 为图的节点总数 */public UF(int n) {// 一开始互不连通this.count = n;// 父节点指针初始指向自己parent = new int[n];for (int i = 0; i < n; i++)parent[i] = i;}/* 其他函数 */
}


如果某两个节点被连通,则让其中的(任意)一个节点的根节点接到另一个节点的根节点上

public void union(int p, int q)
{int rootP = find(p);int rootQ = find(q);if (rootP == rootQ)return;// 将两棵树合并为一棵parent[rootP] = rootQ;// parent[rootQ] = rootP 也一样count--; // 两个分量合二为一
}/* 返回某个节点 x 的根节点 */
private int find(int x)
{// 根节点的 parent[x] == xwhile (parent[x] != x)x = parent[x];return x;
}/* 返回当前的连通分量个数 */
public int count()
{ return count;
}


这样,如果节点p和q连通的话,它们一定拥有相同的根节点:

public boolean connected(int p, int q)
{int rootP = find(p);int rootQ = find(q);return rootP == rootQ;
}


至此,Union-Find 算法就基本完成了。是不是很神奇?竟然可以这样使用数组来模拟出一个森林,如此巧妙的解决这个比较复杂的问题!

那么这个算法的复杂度是多少呢?我们发现,主要 APIconnected和union中的复杂度都是find函数造成的,所以说它们的复杂度和find一样

find主要功能就是从某个节点向上遍历到树根,其时间复杂度就是树的高度。我们可能习惯性地认为树的高度就是logN,但这并不一定。logN的高度只存在于平衡二叉树,对于一般的树可能出现极端不平衡的情况,使得「树」几乎退化成「链表」,树的高度最坏情况下可能变成N

所以说上面这种解法,find,union,connected的时间复杂度都是 O(N)。这个复杂度很不理想的,你想图论解决的都是诸如社交网络这样数据规模巨大的问题,对于union和connected的调用非常频繁,每次调用需要线性时间完全不可忍受。

问题的关键在于,如何想办法避免树的不平衡呢?只需要略施小计即可。

三、平衡性优化

我们要知道哪种情况下可能出现不平衡现象,关键在于union过程:

public void union(int p, int q)
{int rootP = find(p);int rootQ = find(q);if (rootP == rootQ)return;// 将两棵树合并为一棵parent[rootP] = rootQ;// parent[rootQ] = rootP 也可以count--;
}

我们一开始就是 简单粗暴的把p所在的树接到q所在的树的根节点下面那么这里就可能出现「头重脚轻」的不平衡状况,比如下面这种局面


长此以往,树可能生长得很不平衡。我们其实是希望,小一些的树接到大一些的树下面,这样就能避免头重脚轻,更平衡一些。解决方法是额外使用一个size数组,记录每棵树包含的节点数,我们不妨称为「重量」

class UF
{private int count;private int[] parent;// 新增一个数组记录树的“重量”private int[] size;public UF(int n) {this.count = n;parent = new int[n];// 最初每棵树只有一个节点// 重量应该初始化 1size = new int[n];for (int i = 0; i < n; i++) {parent[i] = i;size[i] = 1;}}/* 其他函数 */
}

比如说size[3] = 5表示,以节点3为根的那棵树,总共有5个节点。这样我们可以修改一下union方法:

public void union(int p, int q)
{int rootP = find(p);int rootQ = find(q);if (rootP == rootQ)return;// 小树接到大树下面,较平衡if (size[rootP] > size[rootQ]) {parent[rootQ] = rootP;size[rootP] += size[rootQ];} else {parent[rootP] = rootQ;size[rootQ] += size[rootP];}count--;
}

这样,通过比较树的重量,就可以保证树的生长相对平衡,树的高度大致在logN这个数量级,极大提升执行效率。

此时,find,union,connected的时间复杂度都下降为 O(logN),即便数据规模上亿,所需时间也非常少。

四、路径压缩

这步优化特别简单,所以非常巧妙。我们能不能进一步压缩每棵树的高度,使树高始终保持为常数?


这样find就能以 O(1) 的时间找到某一节点的根节点,相应的,connected和union复杂度都下降为 O(1)。

要做到这一点,非常简单,只需要在find中加一行代码:

private int find(int x)
{while (parent[x] != x) {// 进行路径压缩parent[x] = parent[parent[x]];x = parent[x];}return x;
}

可见,调用find函数每次向树根遍历的同时,顺手将树高缩短了,最终所有树高都不会超过 3(union的时候树高可能达到 3)。

五、总结

我们先来看一下完整代码:

class UF
{// 连通分量个数private int count;// 存储一棵树private int[] parent;// 记录树的“重量”private int[] size;public UF(int n) {this.count = n;parent = new int[n];size = new int[n];for (int i = 0; i < n; i++) {parent[i] = i;size[i] = 1;}}public void union(int p, int q) {int rootP = find(p);int rootQ = find(q);if (rootP == rootQ)return;// 小树接到大树下面,较平衡if (size[rootP] > size[rootQ]) {parent[rootQ] = rootP;size[rootP] += size[rootQ];} else {parent[rootP] = rootQ;size[rootQ] += size[rootP];}count--;}public boolean connected(int p, int q) {int rootP = find(p);int rootQ = find(q);return rootP == rootQ;}private int find(int x) {while (parent[x] != x) {// 进行路径压缩parent[x] = parent[parent[x]];x = parent[x];}return x;}
}

Union-Find 算法的复杂度可以这样分析:构造函数初始化数据结构需要 O(N) 的时间和空间复杂度;连通两个节点union、判断两个节点的连通性connected、计算连通分量count所需的时间复杂度均为 O(1)

  • C++代码
#include <iostream>
#include <vector>
using namespace std;class UnionFind
{                                   public:        UnionFind(size_t N)       {          _arr.resize(N,-1);}                  //找根int FindRoot(int x){                              while(_arr[x] >= 0){                              x = _arr[x];               }return x;}//合并并查集void Union(int x1,int x2){int root1 = FindRoot(x1);int root2 = FindRoot(x2);if(root1 != root2){_arr[root1] += _arr[root2];_arr[root2] = root1;}}      size_t SetSize(){size_t n = 0;for(size_t i = 0;i < _arr.size();i++){if(_arr[i] < 0)n++;}return n;}private:      vector<int> _arr;
};

六、例题


class UnionFind
{                                   public:        UnionFind(size_t N)       {          _arr.resize(N,-1);}                  //找根int FindRoot(int x){                              while(_arr[x] >= 0){                              x = _arr[x];               }return x;}//合并并查集void Union(int x1,int x2){int root1 = FindRoot(x1);int root2 = FindRoot(x2);if(root1 != root2){_arr[root1] += _arr[root2];_arr[root2] = root1;}}      size_t SetSize(){size_t n = 0;for(size_t i = 0;i < _arr.size();i++){if(_arr[i] < 0)n++;}return n;}private:      vector<int> _arr;
};          class Solution {public:int findCircleNum(vector<vector<int>>& M) {UnionFind ufs(M.size());for(size_t i = 0;i < M.size();i++){for(size_t j = 0;j < M[0].size();j++){if(i == j){break;}if(M[i][j])ufs.Union(i,j);}}return ufs.SetSize();}
};



  • 我们可以将每一个变量看作图中的一个节点,把相等的关系 == 看作是连接两个节点的边
  • 那么由于表示相等关系的等式方程具有传递性,即如果 a==b 和 b==c 成立,则 a==c 也成立。
  • 也就是说,所有相等的变量属于同一个连通分量。因此,我们可以使用并查集来维护这种连通分量的关系。
  • 首先遍历所有的等式,构造并查集同一个等式中的两个变量属于同一个连通分量,因此将两个变量进行合并。
  • 然后遍历所有的不等式同一个不等式中的两个变量不能属于同一个连通分量因此对两个变量分别查找其所在的连通分量,如果两个变量在同一个连通分量中,则产生矛盾,返回 false。
  • 如果遍历完所有的不等式没有发现矛盾,则返回 true。
class UnionFind
{                                   public:        UnionFind(size_t N)       {          _arr.resize(N,-1);}                  //找根int FindRoot(int x){                              while(_arr[x] >= 0){                              x = _arr[x];               }return x;}//合并并查集void Union(int x1,int x2){int root1 = FindRoot(x1);int root2 = FindRoot(x2);if(root1 != root2){_arr[root1] += _arr[root2];_arr[root2] = root1;}}      size_t SetSize(){size_t n = 0;for(size_t i = 0;i < _arr.size();i++){if(_arr[i] < 0)n++;}return n;}private:      vector<int> _arr;
};
class Solution {public:bool equationsPossible(vector<string>& equations) {UnionFind ufs(26);for(auto& e : equations){if(e[1] == '='){char ch1 = e[0];char ch2 = e[3];ufs.Union(ch1 - 'a',ch2 - 'a');}}for(auto& e : equations){if(e[1] == '!'){char ch1 = e[0];char ch2 = e[3];int root1 = ufs.FindRoot(ch1 - 'a');int root2 = ufs.FindRoot(ch2 - 'a');if(root1 == root2){return false;}}}return true; }
};

Union-Find 并查集算法详解相关推荐

  1. 并查集-算法详解及例题(最小生成树问题)

    一.并查集的概念: 并查集(Union-find Sets)是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题.一些常见的用途有求连通子图.求最小生成树的 Kruskal 算法和求 ...

  2. 【总结】C++ 高级数据结构 —— 并查集用法详解

    文章目录 一.并查集的介绍 二.并查集的基本操作 一.并查集的介绍 并查集(union_find sets)是一种维护集合的数据结构,它的名字中"并""查"&q ...

  3. B - The Suspects(并查集)详解

    题目描述:n个学生分属m个团体,一个学生可以属于多个团体.一个学生疑似患病则它所属的整个团体都疑似患病.已知0号学生疑似患病,以及每个团体都由哪些学生构成,求一共多少个学生疑似患病. 解题思路:本题的 ...

  4. C++并查集算法(详细)

    C++并查集算法 什么是并查集? 并查集写法 详解 例题:洛谷 P3367.[模板]并查集 题意 代码 什么是并查集? 当我们在做图论题目的时候 经常会读到一些长这样的题目描述: -连接 a , b ...

  5. 基于Excel的查表插值计算工具及算法详解

    一.基于Excel的查表插值计算工具 二维查表算法是控制器软件开发中最为基础的算法之一,同时进行二维查表计算也是标定开发过程中常见操作. 通常一维线性插值算法可以采用手工计算的方式:二维查表插值算法则 ...

  6. 目标检测 RCNN算法详解

    原文:http://blog.csdn.net/shenxiaolu1984/article/details/51066975 [目标检测]RCNN算法详解 Girshick, Ross, et al ...

  7. Popular Cows POJ - 2186(tarjan算法)+详解

    题意: 每一头牛的愿望就是变成一头最受欢迎的牛.现在有 N头牛,给你M对整数(A,B),表示牛 A认为牛B受欢迎.这种关系是具有传递性的,如果 A认为 B受欢迎, B认为 C受欢迎,那么牛 A也认为牛 ...

  8. 图解机器学习算法(13) | 聚类算法详解(机器学习通关指南·完结)

    作者:韩信子@ShowMeAI 教程地址:https://www.showmeai.tech/tutorials/34 本文地址:https://www.showmeai.tech/article-d ...

  9. Apriori算法详解与实现

    Apriori算法详解与实现 一.摘要 二.绪论 三.算法介绍 1.项目 2.项集 3.项集的支持度 4.关联规则 5.关联规则的置信度 6.频繁k项集 7.算法流程 四.代码实现 五.引用 一.摘要 ...

最新文章

  1. 老男孩教育每日一题-第86天-nfs客户端挂载信息写入/etc/fstab中,系统重启,没有自动挂载是什么原因?...
  2. QML基础类型之geopath
  3. Vsftpd文件传输服务(本地用户访问)
  4. VR: AR和VR演进哲学
  5. C++文件读写函数之——fopen、fread和fwrite、fgetc和fputc、fgets和fputs、ftellf和fseek、rewind...
  6. Android让控件位于底部
  7. Linux ALSA 音频系统:物理链路篇
  8. 地图相关知识和地图打印色彩差异解决办法
  9. linux SO文件
  10. 【开源调查问卷系统】limesurvey
  11. android4.4内存,Android 4.4只需512MB内存?别高兴太早
  12. USB转串口那些事儿—USB转串口工作原理及应用
  13. 牵手爱情,绎一份不了情缘
  14. 字符串匹配算法之Aho-Corasick
  15. rails中引入god
  16. syntastic 和 pathogen 安装使用
  17. npm 发布包遇到的问题
  18. sql中把字符串类型转化
  19. 微信更新,重点关注!
  20. 计算机网络常见的协议之ARP协议

热门文章

  1. JAVA学生成绩分析系统任务书,学籍管理系统任务书
  2. 从键盘输入一个正整数(不多于5位),计算该正整数与789之和(记为sum)
  3. 第六章:系统困境之 你的努力与时代进程相逆
  4. 设计模式之工厂模式(三)
  5. 小技巧:with用法 pycharm控制台输出带颜色的文字 打印进度条的
  6. 与粉丝们互动,街头霸王乐队带来AR应用《Gorillaz》
  7. Linux系统管理初步(七)系统服务管理、chkconfig与systemd 编辑中
  8. ACM ICPC 2011-2012 Northeastern European Regional Contest(NEERC)G GCD Guessing Game
  9. POI对Excel自定义日期格式的读取
  10. ehcache + mysql例子与性能测试