本文用来记录对并查集的学习与总结,并通过leetcode
的两道题目来加深对其的理论与实战学习(实现代码Java
)。学习一种数据结构,最高效的方式,就是学以致用,所以这里,以leetcode
的题目为例。
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:”a==b” 或 “a!=b”。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。
示例1:
输入:
["a==b","b!=a"]
输出:false
解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。示例2:
输入:
["b==a","a==b"]
输出:true
解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。
一、并查集介绍
并查集:并查集支持查找
和合并
两种操作的数据结构。并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。主要用于解决一些元素分组的问题。管理一系列不相交的集合。
本质:用集合中的某个元素来代表整个集合,该元素称为集合的代表元。
操作:
- 合并(Union):把两个不相交的集合合并为一个集合。
- 查询(Find):查询两个元素是否在一个集合中。
并查集的基本使用场景:
- 相等传递(例如:等式判断的连通性)。由于相等关系具有传递性,所有相等的变量属于一个集合中。
- 只关心连通性,不关心距离。
具有以上的条件,就可以考虑并查集。
特点:
- 并查集用于判断一个元素是否相连,它们的关系是动态添加的,这一类问题叫做动态连通性问题。
- 主要支持合并与查询是否在同一个集合的操作。
- 底层结构是数组或者哈希表,用于表示节点指向的父节点,初始化时指向自己。
- 合并就是把一个集合的根节点指向另一个集合的根节点,只表示在同一个集合里。
- 这种表示不相交集合的方法称为代表元法,以每个节点的根节点作为一个集合的代表元。
典型应用:最小生成树、Kruskal
算法。
优化:采用压缩算法。路径压缩,按秩压缩。
以上是理论部分:
结合:算法学习笔记:并查集,简单易懂,强烈推荐。
二、题解
针对题一:
==
看作是连接两个节点的边。变量看作是图中的一个节点。所有相等的变量属于同一个连通分量。
- 首先遍历所有的等式,构造并查集。同一个等式中的两个变量属于同一个连通分量,因此将两个变量进行合并。
- 遍历所有的不等式。同一个不相等的两个变量不能属于同一个连通分量,因此对两个变量分别查找其所在的连通分量,如果两个变量在同一个连通分量中,则产生矛盾,返回
false
。 - 实现方式:使用一个数组
parent
存储每个变量的连通分量信息,其中的每个元素表示当前变量所在的连通分量的父节点信息,如果父节点是自身,说明该变量为所在的连通分量的根节点。一开始所有变量的父节点都是它们自身。对于合并操作,我们将第一个变量的根节点的父节点指向第二个变量的根节点;对于查找操作,我们沿着当前变量的父节点一路向上查找,直到找到根节点。
1 | public boolean unionSetMethod(String[] equations){ |
题二:
给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。
另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。
返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。
注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。
示例一:
输入:equations = [[“a”,”b”],[“b”,”c”]], values = [2.0,3.0], queries = [[“a”,”c”],[“b”,”a”],[“a”,”e”],[“a”,”a”],[“x”,”x”]]
输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000]
解释:
条件:a / b = 2.0, b / c = 3.0
问题:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?
结果:[6.0, 0.5, -1.0, 1.0, -1.0 ]实例二:
输入:equations = [[“a”,”b”],[“b”,”c”],[“bc”,”cd”]], values = [1.5,2.5,5.0], queries = [[“a”,”c”],[“c”,”b”],[“bc”,”cd”],[“cd”,”bc”]]
输出:[3.75000,0.40000,5.00000,0.20000]
实现核心:变量和变量之间具有倍数关系。由于变量之间的倍数关系具有传递性,处理具有传递性关系的问题,可以使用并查集
.在并查集的合并
和查询
操作中维护这些变量之间的倍数关系。
构建方式:采用带权的有向图(其中一个边对应一个权值,每个点都有自己的权值,初始化时,默认的权值是1);并且,在维护该并查集时,除了根节点以外,所有同在一个连通分量中的父亲节点均为根节点。
路径压缩:在查询一个结点a
的根节点同时,把节点a
到根节点的沿途所有节点的父亲节点都指向根节点。这样,除了根结点以外,所有结点的父亲结点都指向了根结点。
结果:两个同在一个连通分量中的不同的变量,它们分别到根结点(父亲结点)的权值的比值,就是题目的要求的结果。即先判断两个节点是否在同一个集合中,如果在同一个集合中,就分别取出其对应的weight值,然后求出相应的比值就是结果。
细节点:并查集的查询操作会执行路径压缩。
并查集的特点:一边查询,一边修改节点指向是并查集的特色。(修改的方式:采用路径压缩算法。具体的实现:采用递归的方式)。
tips:
处理数字比处理字母方便的多,因此将变量的值与id进行唯一映射;此后就可以用该id
唯一表示这个变量。
具体的实现步骤:
- 遍历每个等式,并传入相应的
value
值。 - 将该等式中的两个
value
传入相应的集合中。 - 查询给定的
queries
中的值,将结果添加到结果集中。 - 返回结果
UinonFindSet
类的设计模式。
- 初始化构造方法的编写。
union
,合并集合的方法。- 查找方法。边查找,边调整并查集。(采用递归的方式)
1 | public double[] unionSetMethod(List<List<String>> equations, double[] values, List<List<String>> queries) { |
三、总结
初始化:
1 | public void init(int n) |
此时每个节点的父节点都是自身。
查询操作:
1 | public int find(int x) |
递归查询,每个节点都指向其父节点,只有查询到当前集合的根节点时,其父节点是指向其自身,那么就返回当前集合的代表元。即:一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。要判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可。
合并操作:
1 | public void merge(int i, int j) |
合并操作也是很简单的,先找到两个集合的代表元素,然后将前者的父节点设为后者即可。当然也可以将后者的父节点设为前者,这里暂时不重要。这里没有采用压缩算法,最终造成的结果是树的深度很长,形成一条直链,导致查询效率较低。
路径压缩:把沿途的每个节点的父节点都设为根节点。
1 | public int find(int x) |
该算法的核心是,边查找边进行压缩。这里是将每个节点都直接指向该集合中的代表元(通过递归的方式)。
按秩合并:将简单的树往复杂的树上合并,因为这样合并后,到根节点距离变长的节点个数比较少。(这里简单和复杂是指树的高度,高度越高越为复杂)
初始化:(按秩合并)
1 | public void init(int n) |
合并(按秩合并)
1 | public void merge(int i, int j) |