一. 并查集的介绍
1.并查集的简单介绍
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。并查集跟树有些类似,只不过她跟树是相反的。在树这个数据结构里面,每个节点会记录它的子节点。在并查集里,每个节点会记录它的父节点【1】。
并查集的精髓在于,两块集合(区域)有没有相交,以及要不要联通两块区域(集合)。参考树的结构,把元素挂到父节点下,若两个元素的父节点相同,则是同一块区域,两个元素的父节点不相同,则不是同一块区域。
以上图为例,1和2是同一片区域,3和4是同一片区域,5自己是一片区域,那么以上总共有三个区域。
2.并查集的典型使用场景
(1)最大岛屿面积(leetcode695)
0代表水,1代表陆地,求最大的岛屿数量(斜方向不算相交),那么图中最大面积是6(橘黄色)
(2)岛的数量(leetcode200)
这是并查集的典型使用场景,求连通分量,和(1)差不多,但是更简单
(3)省份数量(leetcode547)
在左图中,城市1和城市2是联通的,所以左图共有两个省份。
在右图中,城市和城市都不是联通的,所以共有三个省份。
二. 并查集的主要构成和实现方式
1. 主要构成
(0)总体构成
- 并,代表合并
- 查,代表查找
- 集,代表一个集合,这个集合中,大家的父节点都指向一个地方,即每个节点会记录它的父节点
(1)数据结构实现(HashMap版本)
让子节点指向父节点,可以用HashMap实现,即让Key指向value。
class UnionFind {
private Map<Integer,Integer> father;
}
加入father现在有三个entry,1->2,2->3,4->5.那么它的图示是这样子的:
(2) 元素添加的过程
以用HashMap的方式实现为例,那么添加一个新的元素时,它的父节点是null
public void add(int x){
if(!father.containsKey(x)){//从来没有在father中出现过
father.put(x,null);
numOfSets++;//联通区域需要加1
}
}
添加一个新的元素时,它的父节点也可以是它自己。
public void add(int x){
if(!father.containsKey(x)){//从来没有在father中出现过
father.put(x,x);
}
}
这两种方式都是可以的,下面我们以第一种为例。
添加过程:
在刚开始不发生合并等操作时,自己都是自己的父节点。
(3)查找父节点过程find()
public int find(int x) {
int root = x;
while(father.get(root) != null){
root = father.get(root);
}
return root;
}
层层向上查找,那么找到父节点是null的哪个节点就是祖先了。
(4)缩减层数的过程
这里有一个优化的点:如果我们树很深,比如说退化成链表,那么每次查询的效率都会非常低。所以我们要做一下路径压缩。也就是把树的深度固定为二。【1】
这么做可行的原因是,并查集只是记录了节点之间的连通关系,而节点相互连通只需要有一个相同的祖先就可以了。【1】
把所有是同父节点的节点进行一路压缩,可以减少查询的次数,基本上可变成O(1)的查询时间。
方法一. 层层递归向上,写个while循环或者递归就可以了【1】
public int find(int x) {
int root = x;
while(father.get(root) != null){
root = father.get(root);
}
while(x != root){
int original_father = father.get(x);
father.put(x,root);
x = original_father;
}
return root;
}
方法二. 一路压到栈中,然后层层出栈,把沿路的都挂到父节点上【3】
Stack<Element<V>> path = new Stack<>();
while (element != fatherMap.get(element)) {
path.push(element);
element = fatherMap.get(element);
}
while (!path.isEmpty()) {
fatherMap.put(path.pop(), element);
}
以(3)中图为例,那么压缩路径的图示如下:
(5)并的过程merge()
merge的本质是把两个父节点不同的节点进行合并,从而让他们的父节点相同。
public void merge(int x,int y){
int rootX = find(x);
int rootY = find(y);
if(rootX != rootY){
father.put(rootX,rootY);
numOfSets--;
}
}
以(1)中为例,那么图示为:
2. HashMap实现模板Ref.【3】
模板代码
public class Code04_UnionFind {
public static class Element<V> {
public V value;
public Element(V value) {
this.value = value;
}
}
public static class UnionFindSet<V> {
public HashMap<V, Element<V>> elementMap;
public HashMap<Element<V>, Element<V>> fatherMap;
public HashMap<Element<V>, Integer> rankMap;
public UnionFindSet(List<V> list) {
elementMap = new HashMap<>();
fatherMap = new HashMap<>();
rankMap = new HashMap<>();
for (V value : list) {
Element<V> element = new Element<V>(value);
elementMap.put(value, element);
fatherMap.put(element, element);
rankMap.put(element, 1);
}
}
private Element<V> findHead(Element<V> element) {
Stack<Element<V>> path = new Stack<>();
while (element != fatherMap.get(element)) {
path.push(element);
element = fatherMap.get(element);
}
while (!path.isEmpty()) {
fatherMap.put(path.pop(), element);
}
return element;
}
public boolean isSameSet(V a, V b) {
if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
return findHead(elementMap.get(a)) == findHead(elementMap.get(b));
}
return false;
}
public void union(V a, V b) {
if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
Element<V> aF = findHead(elementMap.get(a));
Element<V> bF = findHead(elementMap.get(b));
if (aF != bF) {
Element<V> big = rankMap.get(aF) >= rankMap.get(bF) ? aF : bF;
Element<V> small = big == aF ? bF : aF;
fatherMap.put(small, big);
rankMap.put(big, rankMap.get(aF) + rankMap.get(bF));
rankMap.remove(small);
}
}
}
}
}
3. HashMap实现 Ref.【1】
模板代码
class UnionFind {
private Map<Integer,Integer> father;
public UnionFind() {
father = new HashMap<Integer,Integer>();
}
public void add(int x) {
if (!father.containsKey(x)) {
father.put(x, null);
}
}
public void merge(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY){
father.put(rootX,rootY);
}
}
public int find(int x) {
int root = x;
while(father.get(root) != null){
root = father.get(root);
}
while(x != root){
int original_father = father.get(x);
father.put(x,root);
x = original_father;
}
return root;
}
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
}
作者:musiala
链接:https://leetcode-cn.com/problems/number-of-provinces/solution/python-duo-tu-xiang-jie-bing-cha-ji-by-m-vjdr/
4. 数组实现 Ref.【2】
// 并查集模板
public class UnionFind {
private int[] parent; // 存储一棵树 记录每个节点的父节点 相当于指向父节点的指针
private int count;// 连通分量总数
// Constructor 初始化
public UnionFind(int n) {
parent = new int[n];
count = n;
for (int i = 0; i < n; i++) {
// 初始化每个节点都指向自己,没有其他连通
parent[i] = i;
}
}
// find , union, isConnected, count
public int find(int x) {
while (x != parent[x]) {
// 压缩路径 优化
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
public void union(int x, int y) {
if (find(x) == find(y)) {
return;
}
// x的根节点 连接 y. 这里没有优化树的平衡,即小树接大树
parent[find(x)] = find(y);
// 合并连接后,连通分量总数减少
count--;
}
// 判断 x&y 是否连通
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
public int getCount() {
// 返回当前的连通分量个数
return count;
}
}
作者:weichen-wang
链接:https://leetcode.cn/problems/number-of-islands/solution/dfs-bfs-unionfindbing-cha-ji-by-weichen-7ldae/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
三. leetcode实战
leetcode547
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。返回矩阵中 省份 的数量.
class Solution {
public int findCircleNum(int[][] isConnected) {
UnionFind uf = new UnionFind();
for(int i = 0; i < isConnected.length;i++){
uf.add(i);
for(int j = 0; j < i; j++){
if(isConnected[i][j] == 1){
uf.merge(i,j);
}
}
}
return uf.numOfSets;
}
}
//首先我们需要一个class结构,来实现并查集结构
class UnionFind{
//HashMap存放每个结点和它的父节点
HashMap<Integer,Integer> father;
//存放联通的区域
int numOfSets = 0;
//初始化
public UnionFind(){
father = new HashMap<>();
numOfSets = 0;
}
//添加结点的过程
public void add(int x){
if(!father.containsKey(x)){//从来没有在father中出现过
father.put(x,null);
numOfSets++;//联通区域需要加1
}
}
//查找的过程
public int find(int x){
int root = x;
while(father.get(root) != null){//在father中一致循环找到它的父节点,层层往上
root = father.get(root);
}
//减层的操作
while(x != root){
int original_father = father.get(x);//先保留父节点
father.put(x,root);//把x直接挂到父节点下
x = original_father;//x变成原来的父节点,然后循环
}
return root;
}
//把两个结点合成一个结点,若父节点一样则不同合了
public void merge(int x,int y){
int rootX = find(x);
int rootY = find(y);
if(rootX != rootY){
father.put(rootX,rootY);
numOfSets--;
}
}
//x和y是否是联通的,找有没有共同的父节点就可以了
public boolean isConnected(int x, int y){
return find(x) == find(y);
}
//返回联通区域的数量
public int getNumOfSets(){
return numOfSets;
}
}
leetcode684
树可以看成是一个连通且 无环 的 无向 图。
给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。
class UnionFind{
HashMap<Integer,Integer> father;
int numset;
public UnionFind(){
numset = 0;
father = new HashMap<>();
}
public void add(int x){
if(!father.containsKey(x)){
father.put(x,null);
numset++;
}
}
public int find(int x){
int root = x;
while(father.get(root) != null){
root = father.get(root);
}
//压缩
while(father.get(x) != null){
int ori_father = father.get(x);
father.put(x,root);
x = ori_father;
}
return root;
}
public void merge(int x, int y){
int rootX = find(x);
int rootY = find(y);
if(rootX != rootY){
father.put(rootX,rootY);
numset--;
}
}
public boolean isConnected(int x, int y){
return find(x) == find(y);
}
public int getnumset(){
return numset;
}
}
class Solution {
public int[] findRedundantConnection(int[][] edges) {
UnionFind uf = new UnionFind();
for(int i = 0; i < edges.length; i++){
if(uf.isConnected(edges[i][0],edges[i][1])){
return edges[i];
}
else{
uf.add(edges[i][0]);
uf.add(edges[i][1]);
uf.merge(edges[i][0],edges[i][1]);
}
}
return edges[0];
}
}