一文掌握并查集算法

欢迎阅读、点赞、转发、订阅,你的举手之间,我的动力源泉。

onepiece-5087720_640.jpg{:width=“400px”}

定义

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。

并查集是一种树型的数据结构,用于处理一些不相交集合 D i s j o i n t Disjoint Disjoint S e t s Sets Sets)的合并及查询问题。常常在使用中以森林来表示。

概念

  • 合并 U n i o n Union Union):把两个不相交的集合合并为一个集合
  • 查询 F i n d Find Find):查询两个元素是否在同一个集合中

伪代码

class UnionFindSet:
	def UnionFindSet(n):
		parents = [0,1...n] # 记录每个元素的parent即根节点 先将它们的父节点设为自己
		ranks =[0,0...0]    # 记录节点的rank值
	
    # 如下图 递归版本 路径压缩(Path Compression)
    # 如果当前的x不是其父节点,就找到当前x的父节点的根节点(find(parents[x])) 并将这个值赋值给x的父节点
	def find(x):
		if ( x !=parents[x]): # 注意这里的if
			parents[x] = find(parents[x])
		return parents[x]

	# 如下图 根据Rank来合并(Union by Rank)
	def union(x,y):
		rootX = find(x) # 找到x的根节点rootX
		rootY = find(y) # 找到y的根节点rootY
        #取rank值小的那个挂到大的那个节点下面,此时两个根节点的rank值并没有发生变化,还是原来的值
		if(ranks[rootX]>ranks[rootY]): parents[rootY] = rootX 
		if(ranks[rootX]<ranks[rootY]): parents[rootX] = rootY
        # 当两个rank值相等时,随便选择一个根节点挂到另外一个跟节点上,但是被挂的那个根节点的rank值需要+1    
		if(ranks[rootX] == ranks[rootY] ):
			parents[rootY] = rootX
			ranks[rootX]++
解释
  • p a r e n t s [ x ] parents[x] parents[x]表示的是 x x x的父节点,初始化时,有一些初始化写法是 p a r e n t s [ x ] parents[x] parents[x]= x x x,表示将 x x x的父节点指向自己

非递归版本find(x),如下图

	def find(x):
		rootX = x # 找到x的根节点
		while (rootX!=parents[rootX]):
			rootX = parents[rootX]
		curr = x # 准备一个curr变量
		while (curr!=rootX):
			next = parents[curr] # 暂存curr的父节点
			parents[curr] = rootX # 将curr节点的父节点设置为rootX
			curr = next # curr节点调到下个节点
        return rootX     

image-20200812094204053.png{:width=“400px”}

根据Rank来合并( U n i o n Union Union b y by by R a n k Rank Rank)

image-20200812090131209.png{:width=“400px”}

路径压缩( P a t h Path Path C o m p r e s s i o n Compression Compression)

image-20200812090146844.png{:width=“400px”}

应用

1.被围绕的区域

image-20200812200206957.png{:width=“400px”}

思路
  • 准备一个并查集 U n i o n F i n d S e t UnionFindSet UnionFindSet,初始化时,多一个节点设置为哑结点 d u m m y dummy dummy
  • 因为是二维矩阵的缘故,可以将其坐标转化为一维矩阵, i ∗ 列 数 + j i * 列数 + j i+j
  • 边缘处的 O O O直接与 d u m m y dummy dummy 进行合并
  • 非边缘的 O O O则需要上下左右四个方向探测,进行合并
  • 遍历,当发现当前节点与 d u m m y dummy dummy节点的根节点相同,即联通的话,这个点维持不变
并查集
        static class UnionFindSet {
            int[] parents;
            int[] ranks;

            public UnionFindSet(int n) {
                parents = new int[n];
                ranks = new int[n];
                for (int i = 0; i < n; i++) {
                    parents[i] = i;
                }
            }


            public int find(int x) {
                if (x != parents[x]) {
                    parents[x] = find(parents[x]);
                }
                System.out.println(x + ":" + parents[x]);
                return parents[x];
            }

            public void union(int x, int y) {
                int rootX = find(x);
                int rootY = find(y);
                if (rootX == rootY) return;
                if (ranks[rootX] > ranks[rootY]) parents[rootY] = rootX;
                if (ranks[rootX] < ranks[rootY]) parents[rootX] = rootY;
                if (ranks[rootX] == ranks[rootY]) {
                    parents[rootY] = rootX;
                    ranks[rootX]++;
                }
            }
        }

主体代码
int m, n;
int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};

public void solve(char[][] board) {
    if (board == null || board.length == 0) return;
    m = board.length;
    n = board[0].length;
    int initValue = m * n + 1;
    UnionFindSet unionFindSet = new UnionFindSet(initValue);
    int dummy = m * n;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (board[i][j] == 'O') {
                if (i == 0 || i == m - 1 || j == 0 || j == n - 1) {
                    unionFindSet.union(node(i, j), dummy);
                } else {
                    for (int k = 0; k < directions.length; k++) {
                        int nextI = i + directions[k][0];
                        int nextJ = j + directions[k][1];
                        if ((nextI > 0 || nextI < m || nextJ > 0 || nextJ < n) && board[nextI][nextJ] == 'O') {
                            unionFindSet.union(node(i, j), node(nextI, nextJ));
                        }
                    }
                }
            }
        }
    }
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (unionFindSet.find(node(i, j)) == unionFindSet.find(dummy)) {
                board[i][j] = 'O';
            } else {
                board[i][j] = 'X';
            }
        }
    }
}

public int node(int i, int j) {
    return i * n + j;
}
2.冗余连接

图传上来就没了,有毒,684题:冗余连接

思路
  • 判断节点第一次出现环的边 e d g e edge edge进行返回,如下图,当 1 1 1的根节点是 4 4 4的时候,从 1 − > 2 − > 3 − > 4 1->2->3->4 1>2>3>4出现一条路径,大概 [ 1 , 4 ] [1,4] [1,4]这个 e d g e edge edge进来后,发现 1 1 1可以直接指向 4 4 4,这时候出现了环,这条边是冗余边

image-20200812192751553.png{:width=“400px”}

    int[] parents;

    public int[] findRedundantConnection(int[][] edges) {
        if (edges == null || edges.length == 0) return new int[]{0, 0};
        int n = edges.length + 1; //注意此处下标多放一个
        init(n);
        for (int[] edge : edges) {
            int x = edge[0], y = edge[1];
            if ((!union(x, y))) {//第二次出现了联通的边时,表示已经找到了
                return edge;
            }
        }
        return new int[]{0, 0};
    }
    //初始化parents
    public void init(int n) {
        parents = new int[n];
        for (int i = 0; i < n; i++) {
            parents[i] = i;
        }
    }

    //递归版路径压缩,找到x的根节点
    public int find(int x) {
        if (x != parents[x]) {
            parents[x] = find(parents[x]);
        }
        return parents[x];
    }

    //改写union方法,第一次当x与y没有联通时,将其设置联通关系,返回ture
    //第二次x和y的跟节点发现一致时,他们已经联通了,返回false
    public boolean union(int x, int y) {
        int rootX = find(x), rootY = find(y);
        if (rootX == rootY) return false;
        parents[rootX] = rootY;
        return true;
    }
番外
//非递归版路径压缩
public int find(int x) {
    int rootX = x;
    while (rootX != parents[rootX]) {
        rootX = parents[rootX];
    }
    int curr = x;
    while (curr != rootX) {
        int next = parents[curr];
        parents[curr] = rootX;
        curr = next;
    }
    return rootX;
}

推荐阅读

推荐阅读

题号链接
130/684一文掌握并查集算法
一文掌握Morris遍历算法
200/-岛屿问题之岛屿的数量[Eighty-eight Butterfly]
493/695岛屿问题之岛屿的周长面积[Morpho Cypris Aphrodite]
130岛屿问题之被围绕的区域[Cicada]
-/-岛屿问题之不同岛屿的数量[Monarch Butterfly]

Reference

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
掌握Ubuntu,您可以采取以下步骤: 1. 下载Ubuntu镜像:您可以从官方网站下载Ubuntu的最新版本。根据您的硬件架构选择合适的版本。 2. 创建启动U盘:使用免费的win32diskimager工具将下载的Ubuntu镜像烧录到U盘上。这样您就可以通过U盘启动安装Ubuntu。将U盘插入计算机,并按照工具的指示进行操作。 3. 安装Ubuntu:将启动U盘插入需要安装Ubuntu的计算机,重新启动计算机。在启动时,选择从U盘启动。按照屏幕上的提示进行Ubuntu的安装过程。您可以选择安装到硬盘上,或者选择试用Ubuntu而不进行实际安装。 4. 更新系统:在安装完成后,建议您更新系统以获取最新的补丁和软件包。打开终端并运行以下命令:sudo apt update && sudo apt upgrade 5. 安装必要的软件:根据您的需求,可以安装各种软件。例如,如果您需要进行深度学习开发,可以安装CUDA和PaddlePaddle。 6. 学习命令行操作:Ubuntu是一个基于Linux的操作系统,使用命令行是非常常见的。您可以学习一些基本的Linux命令,例如件和目录操作、软件包管理等。 7. 探索图形界面:Ubuntu提供了直观的图形界面,您可以通过点击图标和菜单来执行各种操作。尝试打开不同的应用程序,了解它们的功能和用法。 通过以上步骤,您将能够快速上手并掌握Ubuntu操作系统。记得多练习和实践,以加深对Ubuntu的理解和熟练度。祝您成功!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值