并查集介绍-个人总结版

1、基本概念

亲戚问题:有一群人,他们可能属于不同的家族,同一个家族里的人互为亲戚,不同家族的人不是亲戚。已知每个人都知道自己与其他人是否有亲戚关系,求问有几个家族。

并查集:一种描述不相交集合的数据结构,即若一个问题涉及多个元素,它们可以划分到不同集合,同属一个集合内的元素等价,不同集合内的元素不等价。

初始时,问题涉及的元素总是自己构成一个单元集合,求解问题需要通过合并操作将等价元素归入一个集合中。为了能够合并等价元素,我们必须查询希望合并的对象元素属于哪个集合,以决定是否合并。

因此,主要操作就是【查询】和【合并】

2、例题引入并查集

LeetCode 547. 省份数量
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
示例1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
示例2
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3

思路过程:

1、一开始,各个城市之间的是否属于同一个省是未知的,此时每个城市构成单元素集合。

2、遍历信息矩阵,如果(i,j)为1,说明两个城市属于同一个省,可以合并。在集合扩大的过程中,需要找到一个代表,以便多元素之间的合并。所以,确定i和j是否需要合并时,查看它们的代表元素是否相同。

3、矩阵遍历完成之后,再次遍历所有城市。有几个不同的代表,就有几个省份。

//初始化
int[] capital = new int[isConnected.length];
for(int i = 0; i < isConnected.length; i++){
	capital[i] = i;
}

合并

在实现了【查询】方法find(x)后,合并x和y的前提是:find(x) != find(y)。因为如果他们相等,说明两个元素已经在一个集合中了。

对于x,y两个城市,若find(x) != find(y),就令capital[find(y)] = find(x),或者令capital[find(x)] = find(y)。

可能一开始会这么写:capital[y] = x。但这样做只能说明y的代表是x,无法说明y的代表和x的代表相同,在查找find(x)和find(y)后可能出现代表不同的情况,尽管他们已经合并了。

合并x和y就是把y所在的集合合并到x所在的集合中,也就是【使y所在树的根和x所在树的根相同】

public void union(int x,int y){
	if(find(x) != find(y)){
		capital[find(x)] = find(y);
	}
}
public int find(int x){
	if(capaial[x] == x){
		return x;
	}
	return find(capital[x]);
}
//LC.547
class Solution {
    public int findCircleNum(int[][] isConnected) {
        UnionFind uf = new UnionFind(isConnected);
        int n = isConnected.length;
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++){
                if(isConnected[i][j] == 1) uf.union(i,j);
            }
        }
        return n - uf.count;
    }

    class UnionFind{
        int[] capital;
        int count;
        public UnionFind(int[][] isConnected){
            int n = isConnected.length;
            capital = new int[n];
            for(int i = 0; i < n; i++){
                capital[i] = i;
            }
        }

        public int find(int x){
            if(capital[x] == x){
                return x;
            }
            return find(capital[x]);
        }

        public void union(int x,int y){
            if(find(x) != find(y)){
                capital[find(y)] = find(x);
                count++;
            }
        }
    }
}

拓展1:为什么不能通过统计capital数组有几个不同的元素来判断有几个省份?

在合并的过程中,只是把不同集合的代表元素合并在了一起,所以,通过find(x)方法可以找到该元素的代表元素,判断它属于哪个集合。但是,capital[x]说明的是x的代表元素是谁,不是x所在的集合的代表元素。二者的概念是不同的。

find(x) //找到x所在集合的代表
capital[x] //x元素的代表

拓展2:怎么理解更改集合的代表元素(合并)?

初始时,各个元素的代表元素都是指向自身的。合并时,例如:

find(0) != find(1);
capital[find(1)] = find(0);

//此时0和1属于同一个集合,然后合并{0,1}和{2}
//1、若将集合2的代表元素变为集合1的代表元素
//此时元素1和2都指向0,{0,1,2}的代表元素就是0
capital[find(2)] = find(1); // = 0

//2、但如果将集合1的代表元素变为集合2的代表元素
//此时元素1指向0,元素0指向2,2指向自身,这就形成了一个链表状的结构
capital[find(1)] = find(2); // = 2

所以,最坏的情况下,合并完成的集合就是一个链状的树,较高的树将导致较高的查询(及合并)复杂度。

3、求并优化(一)

默认合并时,将y所在树的根指向x树所在的根【capital[find(y)] = find(x)】,最坏的情况下会得到一棵链状的树。那么我们在合并时,先比较两棵树的大小,让较小的树挂到较大的树上,因为较小的树高总是倾向于较低,这样合并后树的高度就不变了。

public void union(int x,int y){
	int xRoot = find(x),yRoot = find(y);
	if(xRoot != yRoot){
		if(size[yRoot] <= size[xRoot]){
			//将y树挂到x树上,更新x树的大小
			parent[yRoot] = xRoot;
			size[xRoot] += size[yRoot];
		}else{
			parent[xRoot] = yRoot;
			size[yRoot] = size[xRoot];
		}
	}
}

新增了一个与parent长度相同的size数组,size[i]表示i结点为根所在树的大小。

int[] size = new int[parent.length];
Arrays.fill(size,1);

此时问题就来了,size的大小并不一定等于树的高度,因此size大的树不一定高于size小的树

4、求并优化(二)

在【路径压缩】之前,可以先用秩(rank)暂时等同于高度(height)

类似【按大小求并】的做法,在判断的时候根据秩(rank)来判断。

public void union(int x,int y){
	int xRoot = find(x),yRoot = find(y);
	if(xRoot != yRoot){
		if(rank[yRoot] <= xRoot){
			parent[yRoot] = xRoot;
		}else{
			parent[xRoot] = yRoot;
		}
		//如果两树的rank相同,自己定义下的y是合并到x树上的,因此x树的rank要++
		if(rank[xRoot] == rank[yRoot]){
			rank[xRoot]++;
		}
	}
}

注意,rank数组保存的是元素i为根的树的高度,但是要查这个集合的高度,应该查rank[find(x)]。

5、求并优化(三)

如果能将链状树中每一个结点的父结点都改为集合的代表元素,那么就完成了【路径压缩】。

我们想到,在查询的过程中,是递归调用find来查询元素的父节点,如果能在查询的过程中完成替换父结点的操作,我们就可以做到路径压缩。

public int find(int x){
	if(parent[x] == x){
		return x;
	}
	return parent[x] = find(parent[x]);
}

例如上图,我们不经过路径压缩的话,合并{1,2,3,5}和{6},{7},{8}集合,就会得到这样的树。

但如果有路径压缩,我们在进行union(5,6)的过程中,查询find(5),就会将parent[5]指向2,而不是1了。

但是路径压缩并不会改变树的秩,find(5)前后的rank(2)依然等于3。

但是经过路径压缩,树的高度已经变成2了。这就是【按秩求并】而非【按高度求并】的原因。

在应用带路径压缩的查询和按秩求并后,rank[root]记录的是树实际高度的上限,树的实际高度可能小于此值。

带路径压缩的按秩求并的并查集代码:

/**
 * 带路径压缩的按秩求并的并查集:最佳优化版
 */
public class UnionFind {
    int[] parent;
    int[] rank;
    //n代表元素的个数,也可以传入关系图 int[][] isConnected
    public UnionFind(int n){
        parent = new int[n];
        for(int i = 0; i < n; i++){
            parent[i] = i;
        }
        rank = new int[n];
        Arrays.fill(rank,1);
    }

    //路径压缩的find
    public int find(int x){
        if(parent[x] == x) return x;
        return parent[x] = find(parent[x]);
    }

    //按秩合并
    public void union(int x,int y){
        int xRoot = find(x), yRoot = find(y);
        if(xRoot != yRoot){
            if(rank[y] <= rank[x]){
                parent[yRoot] = xRoot;
            }else{
                parent[xRoot] = yRoot;
            }

            if(rank[xRoot] == rank[yRoot]){
                rank[xRoot]++;
            }
        }
    }
}

注:文中的扩展均是自己的总结,全文主要内容源自:并查集从入门到出门-yukiyama

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值