连通分量(quick-union)
连通域问题的抽象表述是存在N个节点和M条边,被边直接或间接相连的所有节点共同形成一个域,称为连通域。在进行有限次的连接后,需要快速求出连通域的个数,或者判断任意两个节点的连通性。连通域的个数也称为连通分量,该算法也被称为Union-Find。
例如,下图中的节点就包含三个连通域(红,黑,蓝)。
把节点看作人,把边看作关系,那么连通域就可以用来抽象人群划分问题。把点看作触点,把边看作导线,这就是电路板布线问题。同样连通域也可以用来抽象网络连接问题,用来判断网络中节点的连通性。
在不同的场景下,节点有着不同的具体表示,但是做为算法,我们可以采用更抽象的形式,用0到N-1表示N个节点。我们很容易想到可以用一个数组来表示这N个节点,但是如何在一维数组上构造连接关系需要我们动一点脑筋。
一维数组的每个元素其实都包含着两个信息,一是下标,二是值。下标是固定不可变的信息,我们用它来表示节点,而值就可以用来构造连接关系。在接下来的描述中,我们会频繁使用"节点"和"下标"这两个表述,你需要知道它们是等价的。
连接的含义是通过一个节点可以找到另一个节点。如果我们在某个下标对应的值填入另一个下标,那么就相当于在这两个下标之间建立了一个单向连接。如下图所示,这就是我们需要的全部技巧。
此时判断两个节点是否相连就转换为判断两个下标对应的值是否相等。下标对应的值是什么呢?是另一个下标,或者我们可以称它为父节点。
这样的连接实际上构成了一颗自底向上的树,我们无法从根节点找到叶子节点,但是可以从任意一个节点找到根节点,如果两个节点的根节点相同,那么它们就位于同一颗树,也就是在同一个连通域。对于根节点,我们只需要将它的值设置成它自己的下标即可,也就是让根节点指向它自己。这样,凡是下标与值相等的就是根节点,不相等就是非根节点。当然,设置成一个特殊值,如-1
也是可以的。
构建一颗自顶向下的树至少需要三个域,而构建一颗自底向上的树只需要两个域,因为一个节点可以有任意个子节点,但只能有一个父节点。
下面是一个四个节点的例子,我们依次连接(3,2),(3,1)和(3,0)。
在开始之前我们需要先明确需要做什么,我们至少需要四个操作:
- 连接两个节点:
Union
- 判断两个节点是否相连:
Connected
- 计算连通域个数:
Count
- 寻找一个节点的根节点:
Find
每当我们新增一个有效连接时,都会将两个连通域合并成一个,这种二变一的结果就是相比于连接之前,连通域的个数会减少一个,而初始时,没有任何连接,有多少节点就有多少连通域。因此对于Count
,我们可以记录初始连通域个数,然后在Union
时更新它。就目前为止,我们至少可以写出下面的代码。
type UF struct {n intnode []int
}func New(n int) (uf UF) {uf.n = nuf.node = make([]int, n)for i := 0; i < n; i++ {uf.node[i] = i}return
}func (u UF) Count() int {return u.n
}func (u UF) Connected(p, q int) bool {return u.Find(p) == u.Find(q)
}func (u *UF) Union(p, q int) {if u.Connected(p, q) {return}//do unionu.n--
}func (u *UF) Find(p int) int {//find root of i
}
两个节点的连接是非常简单的,两个连通域的连接也不难,但是这里存在两种选择,不同的选择会带来不同的实现和性能表现。
连接两个连通域也就是合并两棵树,第一种方式是将一棵树的所有节点都挂到另一棵树的根节点上(如下图左),第二种方式是只将根节点挂到另一棵树的根节点上(如下图右)。
第一种方式产生的树只有两层,根节点和叶节点,它非常利于Find
操作,但是不利于Union
操作,因为Union
时需要遍历一棵树。这里实际的操作是遍历数组,因为树只有两层,所以我们遍历数组一次就能找到全部节点。因此,第一种方式的实现也称为quick-find算法。以下是该算法的Union
和Find
的实现。
func (u *UF) Union(p, q int) {pRoot, qRoot := u.Find(p), u.Find(q)if pRoot == qRoot { //已连接return}//将连通域p合并到qfor i := 0; i < len(u.node); i++ {if u.node[i] == pRoot {u.node[i] = qRoot}}u.n--
}func (u *UF) Find(p int) int {return u.node[p] //因为此时的树只有两层
}
第二种方式会产生层次,使树长高。显然它是利于Union
而不利于Find
的,因为Union
操作可以一次完成,但Find
操作可能需要多次访问才能找到根节点,最坏的情况就是树变成单链表。这种方式也称为quick-union算法。以下是算法的Union
和Find
的实现。
func (u *UF) Union(p, q int) {pRoot, qRoot := u.Find(p), u.Find(q)if pRoot == qRoot { //已连接return}//将连通域p合并到qu.node[pRoot] = qRootu.n--
}func (u *UF) Find(p int) int {for p != u.node[p] {p = u.node[p]}return p
}
quick-union算法还有一个问题,就是它"欠扁"。树的高度是影响quick-union算法性能的关键,为了避免quick-union算法中最坏情况的出现,我们需要保证每次连接时都将小树连接到大树上。为此我们需要另一个数组来记录下以每个节点为根节点的树的大小。所以这种优化算法也叫加权quick-union算法,"权"就是一颗树的节点数。
首先我们需要对数据结构做一点小小的修改:
type UF struct {n intnode []intsize []int //记录树大小
}func New(n int) (uf UF) {uf.n = nuf.node = make([]int, n)uf.size = make([]int, n)for i := 0; i < n; i++ {uf.node[i] = iuf.size[i] = 1 //初始时只有一个节点}return
}
下面是加权quick-union算法的Union
的实现。
func (u *UF) Union(p, q int) {pRoot, qRoot := u.Find(p), u.Find(q)if pRoot == qRoot { //已连接return}//将连通域p合并到qif u.size[pRoot] < u.size[qRoot] {u.node[pRoot] = qRootu.size[qRoot] += u.size[pRoot]} else {u.node[qRoot] = pRootu.size[pRoot] += u.size[qRoot]}u.n--
}
还能不能让树再扁平一点呢?
最扁平的树是quick-find算法的树,但是Union
的成本太高,没关系,一招乾坤大挪移将它的成本转嫁给Find
就好了。在Find
操作时,我们增加一个操作,如果当前节点的父节点不是根节点,那么就让该节点指向它的祖父节点。这样Union
可以和加权quick-uion算法一样,我们将原本在Union
中做的扁平化延迟到了Find
中。虽然不能像quick-find一样扁,但是至少比加权quick-uion扁了不少。下面是该算法的Find
的实现。
func (u *UF) Find(p int) int {for p != u.node[p] {u.node[p] = u.node[u.node[p]] //提升p节点的位置p = u.node[p]}return p
}
至此,连通域问题的原理和优化就已经全部介绍完了。在连通域算法中,我们只知道两个节点相连,但是不知道它们如何相连。因为我们在构造树的过程中丢掉了"如何相连"的信息,这也导致了连接无法删除。
连通分量(quick-union)相关推荐
- 【并查集】Union Find
并查集 引出并查集 并查集(Union Find) 如何存储数据? 接口定义 元素的初始化 UnionFind.java Quick Find union 示例及实现 find 实现 Quick Fi ...
- 第三十一篇 玩转数据结构——并查集(Union Find)
1.. 并查集的应用场景 查看"网络"中节点的连接状态,这里的网络是广义上的网络 数学中的集合类的实现 2.. 并查集所支持的操作 对于一组数据,并查集主要支持两种操作:合并两个数 ...
- Leetcode总结之Union Find
package UnionFind;import java.util.ArrayList; import java.util.LinkedList; import java.util.List;pub ...
- 算法学习第一周union find solution
union find 算法是用于解决动态连通性问题的算法,即用于判断一对给定的对象是否相连的问题. 在实现过程中不断改进出现了以下几种实现方法. 一 quick find 算法 在union find ...
- 数据结构与算法(十二)并查集(Union Find)及时间复杂度分析
本文主要包括以下内容: 并查集的概念 并查集的操作 并查集的实现和优化 Quick Find Quick Union 基于size的优化 基于rank的优化 路径压缩优化 并查集的时间复杂度 并查集的 ...
- Union-find
Union-find 终于抽出时间总结回顾一下Union-find了! <算法>书中,从quick_find,quick_union到weighted_quick_union到path c ...
- Algorithms 普林斯顿算法课程笔记(一)
本节将从动态连接性算法(并查集问题的模型)入手,引入算法分析和设计的整体思路和优化方法,为整个课程的引子部分. 主要内容包括 Quick Find和Quick union算法,以及这些算法的改进. 动 ...
- Python JAVA Solutions for Leetcode
Python & JAVA Solutions for Leetcode (inspired by haoel's leetcode) Remember solutions are only ...
- FB面经Prepare: Email User
有一些账号,账号里面有一个或多个email, 如果两个账号有共同的email,则认为这两个账号是同一个人,找出哪些账号是同一个人 输入是这样的:数字是用户,字母是邮箱,有很多人有多个邮箱,找出相同的用 ...
- 261. Graph Valid Tree
题目: Given n nodes labeled from 0 to n - 1 and a list of undirected edges (each edge is a pair of nod ...
最新文章
- struts2拦截器的实现原理及源码剖析
- 发挥数据库价值,企业实现最大数据价值挖掘的路径在这里
- 音视频技术开发周刊 | 228
- Java 获取文件修改时间
- 无法解析的外部符号 __imp__glewinit
- OpenCore引导配置说明第十二版-基于OpenCore-0.6.5正式版
- cropperjs裁剪头像功能实现总结
- ISO20000对高校构建IT服务管理体系的应用价值和实践意义
- Android Binder机制浅析
- c语言课程总结3000字,单片机课程设计心得体会范文3000字
- 可汗学院公开课:线性代数笔记-10-三元线性方程
- sqli-labs 第八关盲注脚本
- Oracle system表空间用满解决
- Apache Shiro 1.2.4 反序列化漏洞(CVE-2016-4437 )
- 基于Android的短信应用开发(六)——将发出短信存至数据库
- 【推荐系统多任务学习 MTL】PLE论文精读笔记(含代码实现)
- Smarty的基本使用与总结
- LDAC在QCC平台支持情况(HIFI选型指南)
- 句柄和句柄类是不同!
- matlab 做偏回归分析,偏最小二乘回归分析|MATLAB 数学统计与优化|MATLAB技术论坛 - Powered by Discuz!...