并查集Leetcode串题三道

并查集Leetcode串题

并查集的核心用途无非就是判断连通区域,具体来说,同一个连通区域的节点都会指向同一个祖先节点,而并查集就是达到这个效果的一个非常简单好用的操作。

首先,我们来熟悉一下并查集的代码,这个代码其实非常简单,主要包含有一个变量ancestors,ancestors[i] = j表示的是第i个节点的祖先是第j个节点。其中有三个方法:初始化方法、寻找祖先节点方法、连接两个节点方法。我们逐一来看:

class UnionFind{
    int[] ancestors;
    public UnionFind(int k){
        //这里的k表示有k个元素
        ancestors = new int[k];
        for(int i = 0; i < k; i++){
            //初始的时候,每个节点的祖先都是自己
            ancestor[i] = i;
        }
	}
    //寻找节点x的祖先节点
    public void find(int x){
        //寻找祖先节点,无非就是递归查看自己的当前祖先节点,再查看当前祖先节点的祖先节点......
        //当x == ancestors[x]时,便可以知道已经到了最顶上的祖先了。
        //所以,我们使用while循环
        while(x != ancestors[x]){
            //这一步进行路径压缩,直接指向自己的”爷爷“节点
            ancestors[x] = ancestors[ancestors[x]];
            x = ancestors[x]}
        return x;
    }
    //连接两个节点:当我们惊奇的发现两个节点是属于同一个连通区域的时候,我们可以调用这个函数,意味着这两个原本分离的连通区域,祖先节点都是同一个,一般地,我们将祖先节点定为索引比较小的节点。
    public void union(int i, int j){
        //寻找二者的祖先节点
        i = find(i);
        j = find(j);
        //将这两个原本分离的连通区域的祖先节点指向同一个(索引较小的节点)
        if(i > j){
            ancestors[i] = j;
        }
        else{
            ancestors[j] = i;
        }
    }
}

以上就是最基本的并查集操作,非常简单明了,下面,我们就以leetcode上面的题目,看看我们具体如何在实际题目中思考、使用并查集进行操作。

例题一 547. 朋友圈

班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。

给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

示例 1:
输入:
[[1,1,0],
[1,1,0],
[0,0,1]]
输出:2 
解释:已知学生 0 和学生 1 互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回 2 。

示例 2:
输入:
[[1,1,0],
[1,1,1],
[0,1,1]]
输出:1
解释:已知学生 0 和学生 1 互为朋友,学生 1 和学生 2 互为朋友,所以学生 0 和学生 2 也是朋友,所以他们三个在一个朋友圈,返回 1 。

提示:
1 <= N <= 200
M[i][i] == 1
M[i][j] == M[j][i]

这道题目中,我们首先应该看到的就是连通区域,虽然没有直接写出来朋友圈就是连通区域,但是,我们可以直观感受到两个不同朋友圈之间相互没有交集,而且,每一个朋友圈中间也可以有一个代表元素作为”祖先节点“。因此在这里,我们快速联想到可以使用并查集的处理方法。题目要我们求的是朋友圈的总数,换句话说,无非要求的就是连通区域的个数,祖先节点的个数。这道题便迎刃而解。

首先,我们可以进行遍历,如果两个学生相应位置为1,说明他们同属于一个朋友圈,我们就应该调用union方法把他们连接起来。遍历完成之后,我们再遍历一次判断存在多少不同的祖先节点就可以了。

注意:并查集的定义大同小异,这里就省略了哈~

class Solution {
    public int findCircleNum(int[][] M) {
        int len1 = M.length;
        if(len1 == 0)   return 0;
        UnionFind uf = new UnionFind(len1);
        for(int i = 0; i < len1; i++){
            for(int j = 0; j < len1; j++){
                if(M[i][j] == 1){
                    uf.union(i, j);
                }
            }
        }
        HashSet<Integer> set = new HashSet<>();
        for(int i = 0; i < len1; i++){
            for(int j = 0; j <len1; j++){
                if(M[i][j] == 1){
                    int x = uf.find(i);
                    set.add(x);
                }
            }
        }
        return set.size();
    }

我们再看下一道题

例题二 200. 岛屿数量

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:
输入:
[
['1','1','1','1','0'],
['1','1','0','1','0'],
['1','1','0','0','0'],
['0','0','0','0','0']
]
输出: 1

示例 2:
输入:
[
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
输出: 3
解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。

这道题中,连通区域的味道也很明显,再矩阵位置上位置联通,意味着他们就应该同属于一个连通区域!而且这道题询问的是岛屿的数量,不就是连通区域的数量嘛!!所以和朋友圈那题大同小异,我们可以火速得到答案。

class Solution {
        public int numIslands(char[][] grid) {
            int len1 = grid.length;
            if(len1 == 0)   return 0;
            int len2 = grid[0].length;
            UnionFind uf = new UnionFind(len1*len2);
            for(int i = 0; i < len1; i++){
                for(int j = 0; j < len2; j++){
                    if(grid[i][j] == '1'){
                        if(i > 0 && grid[i-1][j] == '1'){
                            uf.union(node(i, j, len2), node(i-1, j, len2));
                        }
                        if(j > 0 && grid[i][j-1] == '1'){
                            uf.union(node(i, j, len2), node(i, j-1, len2));
                        }
                    }
                }
            }
            HashSet<Integer> set = new HashSet<>();
            for(int i = 0; i < len1; i++){
                for(int j = 0; j < len2; j++){
                    if(grid[i][j] == '1'){
                        uf.find(node(i, j, len2));
                        set.add(uf.ancester[node(i, j, len2)]);
                    }
                }
            }
            return set.size();
        }

        public int node(int i, int j, int len2){
            return (i)*(len2) + j;
        }

        class UnionFind{
            public int[] ancester;
            public UnionFind(int k){
                ancester = new int[k];
                for(int i = 0; i < k; i++){
                    ancester[i] = i;
                }
            }
            public int find(int x){
                while(x != ancester[x]){
                    ancester[x] = ancester[ancester[x]];
                    x = ancester[x];
                }
                return ancester[x];
            }

            public void union(int i, int j){
                i = find(i);
                j = find(j);
                if(i > j){
                    ancester[i] = j;
                }
                else{
                    ancester[j] = i;
                }
            }
        }
    }

例题三 684. 冗余连接

这是一道压轴题,从难度上要比前面的题目稍稍提高。

在本问题中, 树指的是一个连通且无环的无向图。

输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, …, N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。

返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。

示例 1:

输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
解释: 给定的无向图为:
  1
 / \
2 - 3
示例 2:

输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
解释: 给定的无向图为:
5 - 1 - 2
    |   |
    4 - 3
注意:

输入的二维数组大小在 3 到 1000。
二维数组中的整数在1到N之间,其中N是输入数组的大小。
更新(2017-09-26):
我们已经重新检查了问题描述及测试用例,明确图是无向 图。对于有向图详见冗余连接II。对于造成任何不便,我们深感歉意。

这道题要寻找的是树上的一个环路,题目是这样描述的:树中有一条多余的边,多了这条边之后,树出现了环路。而如何判断是否出现环路呢,我们依据之前并查集的知识,如果两个节点出现在同一个环里,那么我们就可以定义这两个节点是联通状态的,而且他们应该要拥有一个相同的祖先节点。

我们设想一下,如果没有出现环路,那么相连接的两个节点不应该有相同的祖先节点;相反,如果两个待连接的节点已经有一个共同的祖先,也反过来可以得出构成了环路。

依据这个思路,我们就可以轻松得到解题思路,连接两个节点之前,我们可以首先查看它们的祖先节点,如果他们已经隶属于同一个祖先节点,那么这条节点就构成了环路。

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
        int n = edges.length;
        UnionFind uf = new UnionFind(n+1);
        int[] error = null;
        for(int i = 0; i < n; i++){
            if(uf.find(edges[i][0]) == uf.find(edges[i][1])){
                error = edges[i];
            }
            uf.union(edges[i][0], edges[i][1]);
        }
        return error;
    }
    class UnionFind{
        int[] ancestors;
        public UnionFind(int k){
            ancestors = new int[k];
            for(int i = 0; i < k; i++){
                ancestors[i] = i;
            }
        }
        public int find(int x){
            while(x != ancestors[x]){
                ancestors[x] = ancestors[ancestors[x]];
                x = ancestors[x];
            }
            return x;
        }

        public void union(int i, int j){
            i = find(i);
            j = find(j);
            if(i > j){
                ancestors[i] = j;
            }
            else{
                ancestors[j] = i;
            }
        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值