【Leetcode】并查集(Union-Find)算法

并查集(Union-Find)中学习了并查集算法的原理以及几种算法实现。

下面通过leetcode的算法题来具体使用并查集(Union-Find)算法。主要是两种方法来实现并查集:

  1. 使用模板,定义UF类,直接套用模板即可。
  2. 不定义UF类,在函数内部实现初始化,调用find,union函数。

Union-Find模板

1. 使用路径压缩的加权quick-union算法
    class UF{
        int N;
        int count;
        int[] id;
        int[] sz;

        UF(int N){
            this.N = N;
            count = N;
            id = new int[N];
            sz = new int[N];

            for(int i = 0; i < N; i++) {
                id[i] = i;
                sz[i] = 1;
            }
        }

        public int getCount() {
            return count;
        }

        public void union(int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if(pRoot != qRoot) {
                if(sz[pRoot] < sz[qRoot]) {
                    id[pRoot] = id[qRoot];
                    sz[qRoot] += sz[pRoot];
                }else {
                    id[qRoot] = id[pRoot];
                    sz[pRoot] += sz[qRoot];
                }
                count--;
            }
        }

        private int find(int p) {
            if(p == id[p]) return p;
            id[p] = find(id[p]);
            return id[p];
        }
    }

如测试数据量较大,适合用上述模板,若测试数据量较小,使用如下简单版本的并查集:

2. quick-union算法
public class UF {
	private int[] id;    //分量id(以触点作为索引)
	private int count;   //分量数量
	
	public UF(int N) {
		//初始化分量id数组
		count = N;
		id = new int[N];
		for(int i = 0; i < N; i++) {
			id[i] = i;
		}
	}
	
	public int count() {
		return count;
	}
	
	public boolean connected(int p, int q) {
		return find(p) == find(q);
	}
	
	public int find(int p) {
		//找出分量的名称
		while(p != id[p]) p = id[p];
		return p;
	}
	
	public void union(int p, int q) {
		//将p和q的根节点统一
		int pRoot = find(p);
		int qRoot = find(q);
		if(pRoot == qRoot) return;
		
		id[pRoot] = qRoot;
		// id[find(p)] = find(q);
		count--;
	} 
}

1319. 连通网络的操作次数

1. 题目描述

leetcode题目链接:1319. 连通网络的操作次数
在这里插入图片描述在这里插入图片描述

2. 思路分析

利用并查集求出多余的缆线数量和连通分量的个数。

  1. 三个连通块要想联通至少得需要两条边(也就是两条线)那么不难看出最终结果就是连通块数量-1

  2. 注意一个前提也就是线要够(n个节点至少需要n-1条线)

3. 代码实现

使用路径压缩的加权quick-union算法

class Solution {
    public int makeConnected(int n, int[][] connections) {
        if (connections.length < n - 1) return -1; // n 个节点相互连通至少需要n-1条线
        UF uf = new UF(n);
        for (int[] connect : connections) {
            uf.union(connect[0], connect[1]);  // 合并
        }
        return uf.getCount() - 1;
    }
    class UF {  // 路径压缩的加权quick-union算法模板
        int N;
        int count;
        int[] id;
        int[] sz;

        private UF (int n) {
            N = n;
            count = n;
            id = new int[N];
            sz = new int[N];
            for (int i = 0; i < N; i++) {
                id[i] = i;
                sz[i] = 1;
            }
        }

        public int getCount () {
            return count;
        }

        public void union (int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot != qRoot) {
                if (sz[pRoot] < sz[qRoot]) {
                    id[pRoot] = qRoot;
                    sz[qRoot] += sz[pRoot];
                } else {
                    id[qRoot] = pRoot;
                    sz[pRoot] += sz[qRoot];
                }
                count--;
            }
        }

        private int find (int p) {
            if (p == id[p]) {
                return p;
            }
            id[p] = find(id[p]);
            return id[p];
        }
    }
}

quick-union算法

class Solution {
    public int makeConnected(int n, int[][] connections) {
        if (connections.length < n - 1) return -1;
        UF uf = new UF(n);
        for (int[] connect : connections) {
            uf.union(connect[0], connect[1]);
        }
        return uf.count() - 1;
    }
    class UF {  // 算法模板
        private int[] id;    //分量id(以触点作为索引)
        private int count;   //分量数量

        public UF(int N) {
            //初始化分量id数组
            count = N;
            id = new int[N];
            for(int i = 0; i < N; i++) {
                id[i] = i;
            }
        }

        public int count() {
            return count;
        }

        public boolean connected(int p, int q) {
            return find(p) == find(q);
        }

        public int find(int p) {
            //找出分量的名称
            while(p != id[p]) p = id[p];
            return p;
        }

        public void union(int p, int q) {
            //将p和q的根节点统一
            int pRoot = find(p);
            int qRoot = find(q);
            if(pRoot == qRoot) return;
            
            id[pRoot] = qRoot;
            // id[find(p)] = find(q);
            count--;
        } 
    }
}

这个就比较耗时,因此使用更简单的并查集写法,不创建并查集的类,直接在函数中实现:

class Solution {
    int[] id;
    public int makeConnected(int n, int[][] connections) {
        if (connections.length < n - 1) return -1;
        // 初始化
        id = new int[n];
        for (int i = 0; i < n; i++) {
            id[i] = i;
        }
        for (int[] connect : connections) {
            union(connect[0], connect[1]);
        }
        // 这里也可以定义count函数,就不用再写for循环,同上
        int count = 0;
        for (int i = 0; i < n; i++) {
            if (id[i] == i) {
                count++;
            }
        }
        return count - 1;
    }
    // find、union函数直接从类中复制即可
    private int find(int p) {
        //找出分量的名称
        while(p != id[p]) p = id[p];
        return p;
    }

    private void union(int p, int q) {
        //将p和q的根节点统一
        int pRoot = find(p);
        int qRoot = find(q);
        if(pRoot == qRoot) return;
        
        id[pRoot] = qRoot;
    } 
}

union函数,getCount函数,初始化都在函数内部实现。

class Solution {
    public int makeConnected(int n, int[][] connections) {
        int[] id = new int[n];
        for (int i = 0; i < n; i++) {
            id[i] = i;
        }
        int count = n, res = 0;
        for (int[] e : connections) {
            int p1 = find(id, e[0]);
            int p2 = find(id, e[1]);
            if (p1 == p2) {
                res++;
            } else {
                count--;
                id[p1] = p2;
            }
        }
        
        return res + 1 >= count ? count - 1 : -1;
    }

    private int find(int[] id, int p) {
        if (p == id[p]) return p;
        id[p] = find(id, id[p]);
        return id[p];
    }
}

547. 省份数量

1. 题目描述

leetcode题目链接:547. 省份数量
在这里插入图片描述在这里插入图片描述

2. 思路分析

使用并查集算法,如果两者之间为1,则合并在一块,最后返回count。

3. 代码实现

模板实现

class Solution {
    public int findCircleNum(int[][] isConnected) {
        int m = isConnected.length, n = isConnected[0].length;
        if (m == 0 || n == 0) return 0;
        UF uf = new UF(m);
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (isConnected[i][j] == 1) {
                    uf.union(i, j);
                }
            }
        }
        return uf.getCount();

    }
    class UF {  // 路径压缩的加权quick-union算法模板
        int N;
        int count;
        int[] id;
        int[] sz;

        private UF (int n) {
            N = n;
            count = n;
            id = new int[N];
            sz = new int[N];
            for (int i = 0; i < N; i++) {
                id[i] = i;
                sz[i] = 1;
            }
        }

        public int getCount () {
            return count;
        }

        public void union (int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot != qRoot) {
                if (sz[pRoot] < sz[qRoot]) {
                    id[pRoot] = qRoot;
                    sz[qRoot] += sz[pRoot];
                } else {
                    id[qRoot] = pRoot;
                    sz[pRoot] += sz[qRoot];
                }
                count--;
            }
        }

        private int find (int p) {
            if (p == id[p]) {
                return p;
            }
            id[p] = find(id[p]);
            return id[p];
        }
    }
}

不用模板,函数内部实现

class Solution {
    int[] id;
    int count = 0;
    public int findCircleNum(int[][] isConnected) {
        int m = isConnected.length, n = isConnected[0].length;
        if (m == 0 || n == 0) return 0;
        count = m;
        id = new int[m];
        for (int i = 0; i < m; i++) id[i] = i;  // 初始化
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (isConnected[i][j] == 1) union(i, j);   
            }
        }
        return count;
    }
    // find、union函数直接从类中复制即可
    private int find(int p) {
        if (p == id[p]) return p;
        id[p] = find(id[p]);
        return id[p];
    }
    
    private void union(int p, int q) {
        int pid = find(p);
        int qid = find(q);
        if (pid == qid) return;
        id[pid] = qid;
        count--;
    }   
}

959. 由斜杠划分区域

1. 题目描述

leetcode题目链接:959. 由斜杠划分区域
在这里插入图片描述
在这里插入图片描述

2. 思路分析

在这里插入图片描述
连接时需要考虑以下5种情况:

  • 字符为空格“ ”:此时需连接区域[1, 2, 3, 4]
  • 字符为斜杠"/":此时需连接区域[0, 3], [1, 2]
  • 字符为反斜杠"\":此时需连接区域[0, 1],[2, 3]
  • 考虑方格右端(如图中黄色区域所示)[1, 1 + 4 + 3]
  • 考虑方格下端(如图中青色区域所示)[2, 4 * n]
3. 代码实现

模板实现

class Solution {
    public int regionsBySlashes(String[] grid) {
        int n = grid.length;
        int size = 4 * n * n;
        UF uf = new UF(size);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                int k = 4 * i * n + 4 * j;
                if (grid[i].charAt(j) == ' ') {
                    uf.union(k, k + 1);
                    uf.union(k + 1, k + 2);
                    uf.union(k + 2, k + 3);
                } else if (grid[i].charAt(j) == '/') {
                    uf.union(k, k + 3);
                    uf.union(k + 1, k + 2);
                } else {
                    uf.union(k, k + 1);
                    uf.union(k + 2, k + 3);
                }
                if (j < n - 1) {
                    uf.union(k + 1, k + 4 + 3);
                }
                if (i < n - 1) {
                    uf.union(k + 2, k + n * 4);
                }
            }
        }
        return uf.getCount();

    }
    class UF {  // 路径压缩的加权quick-union算法模板
        int N;
        int count;
        int[] id;
        int[] sz;

        private UF (int n) {
            N = n;
            count = n;
            id = new int[N];
            sz = new int[N];
            for (int i = 0; i < N; i++) {
                id[i] = i;
                sz[i] = 1;
            }
        }

        public int getCount () {
            return count;
        }

        public void union (int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot != qRoot) {
                if (sz[pRoot] < sz[qRoot]) {
                    id[pRoot] = qRoot;
                    sz[qRoot] += sz[pRoot];
                } else {
                    id[qRoot] = pRoot;
                    sz[pRoot] += sz[qRoot];
                }
                count--;
            }
        }

        private int find (int p) {
            if (p == id[p]) {
                return p;
            }
            id[p] = find(id[p]);
            return id[p];
        }
    }
}

1579. 保证图可完全遍历

1. 题目描述

leetcode题目链接:1579. 保证图可完全遍历
在这里插入图片描述
在这里插入图片描述

2. 思路分析
  1. 贪心思路,优先处理公共边,将公共边信息保存在并查集中,并计算多余的公共边(处于同一个连通分量);
  2. 然后对Alice和Bob分别处理(注意此时应在各自的并查集中执行操作),因为图可完全遍历等价于图中只存在一个连通分量,此时对其各自操作,边的顺序并不影响结果,累加多余的边(处于同一个连通分量)。
  3. 最后判断能否完全遍历即可。

需要注意的是,题目中的节点不是从0开始的,为了符合并查集的定义初始化,先将节点编号改为从0开始。

3. 代码实现
class Solution {
    public int maxNumEdgesToRemove(int n, int[][] edges) {
        // 先将节点编号改为从0开始,或者大小设为n+1
        for (int[] edge : edges) {
            edge[1]--;
            edge[2]--;
        }
        UF ufa = new UF(n);
        UF ufb = new UF(n);
        int res = 0;
        // 优先处理公共边
        for (int[] edge : edges) {
            if (edge[0] == 3) {
                if (!ufa.union(edge[1], edge[2])) {
                    res++;
                } else {
                    ufb.union(edge[1], edge[2]);
                }
            }
        }
        // 分别处理单独边
        for (int[] edge : edges) {
            if (edge[0] == 1) {
                if (!ufa.union(edge[1], edge[2])) {
                    res++;
                }
            }
            if (edge[0] == 2) {
                if (!ufb.union(edge[1], edge[2])) {
                    res++;
                }
            }
        }
        return ufa.getCount() == 1 && ufb.getCount() == 1 ? res : -1;

    }
    class UF {
        int count;   //连通分量个数
        int[] id;
        int[] sz;

        public UF(int n) {
            count = n;
            id = new int[n];
            sz = new int[n];
            for (int i = 0; i < n; i++) {
                id[i] = i;
                sz[i] = 1;
            }
        }

        public int getCount() {
            return count;
        }

        public int find(int p) {
            if (p != id[p]) {
                id[p] = find(id[p]);
            }
            return id[p];
        }

        public boolean union(int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot == qRoot) {
                return  false;
            }
            if (sz[pRoot] > sz[qRoot]) {
                id[qRoot] = pRoot;
                sz[pRoot] += sz[qRoot];
            } else {
                id[pRoot] = qRoot;
                sz[qRoot] += sz[pRoot];
            }
            count--;
            return true;
        }
    }
}

803. 打砖块

1. 题目描述

leetcode题目链接:803. 打砖块
在这里插入图片描述
在这里插入图片描述

2. 思路分析

此题考察的是逆向思维。

这题只需求每次打击位置后掉落的砖块数目即可,因此可将矩阵中第一行(最顶端位置的砖块)汇聚到根节点,逆向求解是,先使用一个临时副本矩阵,对此进行操作,将打击掉后的砖块状态存入并查集中,然后逆向求解每次将砖块补齐后,前后根节点连接子树的大小差值。

可以参考:打砖块:官方视频讲解

3. 代码实现
class Solution {
    public int[] hitBricks(int[][] grid, int[][] hits) {
        int m = grid.length, n = grid[0].length;
        if (grid == null || m == 0 || n == 0) {
            return new int[]{};
        }
        int[][] copy = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                copy[i][j] = grid[i][j];
            }
        }
        // 1. 击碎
        for (int[] hit : hits) {
            copy[hit[0]][hit[1]] = 0;
        }
        // 2. 建图连接
        int size = m * n;
        UF uf = new UF(size + 1);
        //顶层初始化
        for (int i = 0; i < n; i++) {
            if (copy[0][i] == 1) {
                uf.union(size, i);
            }
        }

        for (int i = 1; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (copy[i][j] == 1) {
                    int con = i * n + j;
                    //看上边
                    if (copy[i - 1][j] == 1) {
                        uf.union(con, con - n);
                    }
                    //看左边
                    if (j > 0 && copy[i][j - 1] == 1) {
                        uf.union(con, con - 1);
                    }
                }
            }
        }
        // 3. .逆序补回砖块
        int[] dirs = {-1, 0, 1, 0, -1};
        int len = hits.length;
        int[] res = new int[len];
        for (int i = len - 1; i >= 0; i--) {
            int x = hits[i][0], y = hits[i][1];
            int loc = x * n + y;
            if (grid[x][y] == 0) {
                continue;
            }
            int origin = uf.getSize(size);
            if (x == 0) {
                uf.union(size, loc);
            }
            //观察四个方向,将连接的网格合并
            for (int k = 0; k < 4; k++) {
                int newx = x + dirs[k], newy = y + dirs[k + 1];
                if (newx < 0 || newx >= m || newy < 0 || newy >= n) {
                    continue;
                }
                int newloc = newx * n + newy;
                if (copy[newx][newy] == 1) {
                    uf.union(loc, newloc);
                }
            }

            int current = uf.getSize(size);
            res[i] = Math.max(0, current - origin - 1);
            //补上砖块
            copy[x][y] = 1;
        }
        return res;
    }
    class UF {
        int[] id;
        int[] sz;

        UF (int n) {
            id = new int[n];
            sz = new int[n];
            for (int i = 0; i < n; i++) {
                id[i] = i;
                sz[i] = 1;
            }
        }

        public int getSize(int p) {
            int root = find(p);
            return sz[root];
        }

        public int find(int p) {
            if (p != id[p]) {
                id[p] = find(id[p]);
            }
            return id[p];
        }

        public void union(int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot == qRoot) return;
            id[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }
    }
}

参考:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值