[杂记]算法: 并查集

news2024/11/18 6:19:53

0. 引言

我们考虑如何计算一个图连通分量的个数. 假定简单无向图 G G G有两个连通分量(子图) G 1 , G 2 G_1, G_2 G1,G2, 如下图所示:
在这里插入图片描述

一个很自然的想法是, 要想求连通分量个数, 我们可以使用Full-DFS算法, 也就是我们从某个点开始深度优先搜索, 并标记访问过的元素. 随后挨个顶点判断, 如果某个点没有被访问过, 则接着从该点进行深度优先搜索, 这样深度优先搜索的次数就是连通量的个数.

除此之外, 我们还可以用并查集来求图中连通分量的个数. 并查集, 顾名思义, 有并与查两部分. 并, 就是合并(join, union), 即利用某种规则将两个顶点合并为一个连通分量. 查, 就是查询(find), 用以查询某个节点所属连通分量的代表性节点.

例如, 某个单位里, 有两个帮派. 一个员工A和另一个员工B相遇了, 两个人想知道自己是不是同属一个帮派. 于是, 两个人可以互相报上自己帮派的老大, 是局长还是书记. 如果两个人的老大一样, 则两个人就是同属一个帮派, 否则不是.

抽象出来, 我们将每个连通分量都选出一个代表的节点, 这个节点就是老大, 该连通分量中其他的节点或直接, 或间接地与老大相连. 一个单位帮派的个数可以用老大的个数来代表.

下面说具体如何去并, 查

1. 查, find

一个连通图总可以表示成一个生成树(例如, 单位里的一个帮派总可以捋出来一个上下级关系), 例如, G 1 G_1 G1子图中, 假设每个节点有编号, 那么假定有如图的一种连接关系(生成树):
在这里插入图片描述
1是老大, 下面是2, 3, 4是老末. 如果我们用一个数组 p a r e n t [ N ] parent[N] parent[N]表示这种上下级关系, 也就是 p a r e n t [ i ] = j parent[i]=j parent[i]=j表示i的上级为j, 则上图的关系可以表示为:

parent[4] = 2
parent[2] = 1
parent[3] = 1
parent[1] = 1

所以, 只有老大的上级才是他自己. 如果给我们一个4, 想查一下, 4的老大是谁? 我们就可以这样一层一层查上去:

parent[4] = 2
parent[2] = 1
parent[1] = 1, Done!!!

当找到满足parent[i] = i的i, 就表示找到了老大(子图的代表)为i.

我们可以用代码这样表示:

int find(int x) {
	while (parent[x] != x) x = parent[x];  // 只要没找到, 就沿着上级继续找
	return x;  // 找到了, 返回
}

但是, 假如一个倒霉催的结构是这样的:

在这里插入图片描述
那我们就需要花费 O ( n ) O(n) O(n)的时间去找, 这是因为不平衡的树会带来更大的时间复杂度(最坏的情况为 O ( n ) O(n) O(n)). 为此, 既然我们只是找老大, 我们直接让每个人直接对老大负责不好么?

在这里插入图片描述
我们如何做到这一点? 我们用递归来实现. 对于不满足parent[i] = i的i, 我们直接让i的上级parent[i]等于老大, 也就是parent[i] = find(parent[i]), 这样递归下去就可以达到上图的结果. 需要说明的是, 这样整理好之后, 以后的查询才会省时间. 换言之, 第一次是不会节约时间的. 于是现在的find函数可以写为:

int find(int i) {
	if (parent[i] != i)   // 没有找到
		parent[i] = find(parent[i]);  // 让i的上级是老大
	
	return parent[i];  // 返回老大
}

2. 并, union, join

如果说, 单位里的两个派别准备重修旧好, 也就是两个派别要合并. 然而, 一个派别只能有一个老大, 所以两个派别的老大就必须有一个要屈尊, 不再当老大, 成为另一个老大的下属, 这样的话, 不论两个派别的哪个人, 最终都可以查询到新的老大, 这就是并的过程, 用下图表示:

在这里插入图片描述

代码这么来描述:

void unoin(int x, int y){  // 合并俩派别, x, y为其中的成员
	int px = find(x), py = find(y);  // 找到他们各自的老大
	if (px != py) parent[px] = py;  // 让px的老大的老大为py
}

那么假如我们想指定一种合并的规则, 应该如何做呢? 假设我们标记节点的深度, 如图所示:

在这里插入图片描述
因此, 为了不让树产生退化(合并后左右子树的高度差尽可能小), 我们将深度小的合并到深度大的. 如果二者的深度相同, 则可以随便指定一个作为新老大, 随后将新老大的权值加1, 如图:
在这里插入图片描述

代码如下:

void unoin(int x, int y) {
	// 寻找x, y的老大
	int px = find(x), py = find(y);
	if (px != py) {
	
		// 如果x的权重大 让y的老大为x
		if (rank[px] > rank[py]) parent[py] = px;
		else {  // 否则 让x的老大为y
			if (rank[px] == rank[py]) rank[py]++; // 如果权重相等 则先将y的权重加1
			parent[px] = py;
		}
	}
}

3. 例题

下面用几道例题说明并查集的使用. 难度从低到高.

3.1 最长连续序列

题目链接: LeetCode128

在这里插入图片描述
这个题让我们在数组中找到一个最长的连续数字序列. 如果我们将元素视为图里的节点, 每一段连续的数字序列作为一个子图, 就是要找到节点最多的子图.

为此, 我们对于数组元素num, 假如num + 1也在数组中, 则将num所在的子图与num + 1所在的子图并在一起. 除此之外, 我们还需要用一个额外的哈希表记录每个子图的大小, 键为子图的老大, 值为子图的大小.

代码:

class Solution {
public:
    unordered_map<int, int> pre, regionResult;  // pre: 储存父节点 regionResult: 节点所在连通域面积

    int find(int x) {  // 查
        if (x == pre[x]) return x;
        else {
            pre[x] = find(pre[x]);
            return pre[x];
        }

    }

    int merge(int x, int y) {  // 并
        x = find(x), y = find(y);
        if (x == y) return regionResult[x];

        // 若不相等 并到一起
        pre[y] = x;
        regionResult[x] += regionResult[y];  // 把大小加一起并返回
        return regionResult[x];
    }

    int longestConsecutive(vector<int>& nums) {
        if (!nums.size()) return 0;
        int result = 1;
        
        // 初始化
        for (int num : nums) {
            pre[num] = num;
            regionResult[num] = 1;
        }

        // 合并相邻数 并更新结果 注意只合并num与num+1 不需要合并num与num-1 否则重复

        for (int num : nums) {
            if (pre.count(num + 1))
                result = max(result, merge(num, num + 1));
        }

        return result;
    }
};

3.2. 危险程度

题目链接: Acwing4796

在这里插入图片描述
这个题是说, 有一个试管, 初始时危险值为1. 现在往里面导入物质, 假如要倒入的物质可以和试管里面任一一种物质反应, 则将危险值乘2. 问可以达到的最大危险值为多少.

我们可以构建图来解决这个问题. 假如a与b反应, b与c反应, 则a, b, c可以构成一个连通子图. 于是n个物质就构成了若干个连通子图, 每个子图中的物质可以与子图中其他若干个物质发生反应. 所以, 对于一个有t个节点的子图, 可以达到最大的危险值为 2 t − 1 2^{t-1} 2t1, 证明如下:

例如假设四种物质a,b,c,d的反应关系如下(如果不是严格的树(即有环), 也一定可以等价成如下的形式):
在这里插入图片描述
那么只要按照广度优先搜索的顺序倒入a->b->c->d, 就可以达到2^3=8的危险度. 且不同子图间是孤立的, 因此假设一共有k个子图, 每个子图的节点个数分别为 a 0 , . . . , a k − 1 a_0, ..., a_{k-1} a0,...,ak1, 则最大的危险程度为
2 a 0 − 1 ⋅ 2 a 1 − 1 ⋅ ⋅ ⋅ ⋅ 2 a k − 1 − 1 = 2 n − k 2^{a_0-1}·2^{a_1-1}····2^{a_{k-1}-1}=2^{n-k} 2a012a11⋅⋅⋅⋅2ak11=2nk

为此, 我们只需要求出连通子图的个数即可.

代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;

int parent[N];  // parent数组

int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]);
    }
    
    return parent[x];
}
int main()
{
    int n, m;
    cin >> n >> m;
    
    // 初始化并查集 每个指向自己
    for (int i = 0; i < n; i ++ )
        parent[i] = i;
        
    int result = n;  // 初始n个子图
    while (m -- ) {
        int x, y;  
        cin >> x >> y;  // 能反应的两种物质
        
        // 找到老大
        x = find(x); y = find(y);
        
        if (x != y) {  // 合并两个子图
            parent[x] = y;
            result --;
        }
        
    }
    
    cout << (1LL << (n - result)) << "\n";
    return 0;
    
}

3.3. 岛屿数量

题目链接: LeetCode200

在这里插入图片描述
这个题的一种做法是, 跟玩扫雷一样, 但凡是扫到一个1, 那么执行深度优先搜索, 将与其相连的1都扫成0. 这样再碰到下一个1时, 一定是另一个岛屿, 所以岛屿的数量就是执行深度优先搜索的次数. 代码:

class Solution {
private:
    int rows = 0, cols = 0;
public:
    void dfs(vector<vector<char>>& grid, int row, int col) {

        grid[row][col] = '0';  // 标记为0
        // 上下左右, 深度优先
        if (row > 0 && grid[row - 1][col] == '1') dfs(grid, row - 1, col);
        if (col > 0 && grid[row][col - 1] == '1') dfs(grid, row, col - 1);
        if (row < rows - 1 && grid[row + 1][col] == '1') dfs(grid, row + 1, col);
        if (col < cols - 1 && grid[row][col + 1] == '1') dfs(grid, row, col + 1); 

    }
    int numIslands(vector<vector<char>>& grid) {
        this->rows = grid.size();
        this->cols = grid[0].size();
        int result = 0;

        for (int row = 0; row < rows; row ++) {
            for (int col = 0; col < cols; col ++) {
                if (grid[row][col] == '1') {
                    dfs(grid, row, col);
                    result ++;
                }
            }
        }

        return result;
    }
};

也可以用并查集来做. 每个'1', 我们都将其与周围的'1'连通, 则岛屿的数量就是连通图的数量.

class UnionFind {
public:
    UnionFind(vector<vector<char>>& grid) {
        count = 0;
        int m = grid.size();
        int n = grid[0].size();
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == '1') {
                    parent.push_back(i * n + j);
                    ++count;
                }
                else {
                    parent.push_back(-1);
                }
                rank.push_back(0);
            }
        }
    }

    int find(int i) {
        if (parent[i] != i) {
            parent[i] = find(parent[i]);
        }
        return parent[i];
    }

    void unite(int x, int y) {
        int rootx = find(x);
        int rooty = find(y);
        if (rootx != rooty) {
            if (rank[rootx] > rank[rooty])
                parent[rooty] = rootx;
            else {
                if (rank[rootx] == rank[rooty]) rank[rooty] ++;
                parent[rootx] = rooty;
            }
            count --;
        }
    }

    int getCount() const {
        return count;
    }

private:
    vector<int> parent;
    vector<int> rank;
    int count;
};

class Solution {
public:
    int numIslands(vector<vector<char>>& grid) {
        int nr = grid.size();
        if (!nr) return 0;
        int nc = grid[0].size();

        UnionFind uf(grid);
        int num_islands = 0;
        for (int r = 0; r < nr; ++r) {
            for (int c = 0; c < nc; ++c) {
                if (grid[r][c] == '1') {
                    grid[r][c] = '0';
                    if (r - 1 >= 0 && grid[r-1][c] == '1') uf.unite(r * nc + c, (r-1) * nc + c);
                    if (r + 1 < nr && grid[r+1][c] == '1') uf.unite(r * nc + c, (r+1) * nc + c);
                    if (c - 1 >= 0 && grid[r][c-1] == '1') uf.unite(r * nc + c, r * nc + c - 1);
                    if (c + 1 < nc && grid[r][c+1] == '1') uf.unite(r * nc + c, r * nc + c + 1);
                }
            }
        }

        return uf.getCount();
    }
};


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/160117.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高等数学(第七版)同济大学 总习题十一 个人解答

高等数学&#xff08;第七版&#xff09;同济大学 总习题十一 函数作图软件&#xff1a;Mathematica 1.填空&#xff1a;\begin{aligned}&1. \ 填空&#xff1a;&\end{aligned}​1. 填空&#xff1a;​​ (1)第二类曲线积分∫ΓPdxQdyRdz化成第一类曲线积分是_____&am…

Yarn 下载安装及常用配置和命令总结

title: Yarn 下载安装及常用配置和命令总结 date: 2023-01-13 14:47:32 tags: 开发工具及环境 categories:开发工具及环境 cover: https://cover.png feature: false 1. Node.js 建议先安装好 Node.js&#xff0c;见另一篇&#xff1a;Node.js 多版本安装及 NPM 镜像配置_凡 …

Materials - 角色分层材质规范

之前编写的解释性文档&#xff0c;归档发布&#xff1b;在传统贴图中&#xff0c;以BaseColor贴图为例&#xff0c;我们将几乎所有纹理信息都集中到一张贴图上&#xff0c;比如下图中&#xff0c;就有金属、皮革和布料等各种质感的纹理信息&#xff1a;即使是4K的贴图&#xff…

在Win10下装VMware17后,[ 安装VMware Tools ]选项灰色的解决办法

一、说明 菜单【虚拟机】【安装VMware Tools】按钮为灰色&#xff0c;无法实现【安装VMware Tools】的功能&#xff0c;如何解决&#xff0c;使这个功能可以实现&#xff1f;本文介绍此过程。 二、问题发现 在Win10下安装Vmware17后&#xff0c;生成ubuntu18的虚拟机&#xff…

基于java(springboot+mybatis)汽车信息管理系统设计和实现以及文档

基于java(springbootmybatis)汽车信息管理系统设计和实现以及文档 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言…

行业分享:光伏行业如何利用视觉检测系统降本增效?

导语&#xff1a;机器视觉检测已在光伏产品生产的各个环节中&#xff0c;为产品产量与质量提供可靠保障。维视智造作为光伏组件视觉检测系统领先者&#xff0c;为企业提供专业、系统、稳定的光伏组件视觉检测解决方案&#xff0c;可保证0漏检&#xff0c;全面提升生产效率。一、…

C++:C++编译过程:看完还不懂C++编译过程来捶我

1&#xff1a;先看图 2&#xff1a;一个C源文件从文本到可执行文件经历的过程&#xff1a; gcc Hello.cpp 预处理阶段&#xff1a;gcc -E hello.c -o hello.i 对源代码文件中包含关系&#xff08;头文件&#xff09;&#xff0c;预编译语句&#xff08;宏定义&#xff09…

React中如何拆分组件

基于自己工作中的体会&#xff0c;还有在做的过程中翻阅的资料&#xff0c;看的资料没有收藏起来&#xff0c;很想指出具体的出处&#xff0c;但是很多都是从各个地方看到的。不过都是在掘金、公众号前端开发、还有知乎上看到的。 &#x1f62b; 前言 随着web业务越来越复杂&a…

Elasticsearch(一)--Elasticsearch概述

一、前言 从本章开始&#xff0c;我将进入elasticSearch&#xff08;后面简称es&#xff09;的学习&#xff0c;同样也是通过书籍自学&#xff0c;并且会通过自己归纳和拓展将我觉得比较值得记录的知识点分享出来&#xff0c;如果大家觉得有用的话可以和我一起学习。我打算在总…

Kotlin

目录 一、Kotlin 基础语法 1、方法函数 2、常量 val 和变量 var 3、${} 字符串模板 4、null 处理 !!. 不能为空 ?.为空不处理 ?:为空处理成 5、is 类型转换 相当于 instanceof 6、Any 相当于 Java的 Object 二、Kotlin 基本数据类型 1、基本数据类型&#xf…

AMD出招,英特尔最不想看到的对手来了

前段时间的CES上&#xff0c;AMD正式发布Ryzen 7000的3D缓存版&#xff0c;对于游戏玩家来说&#xff0c;Ryzen 7000 3D缓存版算是今年最期待的CPU。上一代的Ryzen7 5800X3D凭借超强的游戏性能和性价比&#xff0c;在德国最大的PC硬件零售商的统计中&#xff0c;甚至成为2022年…

高并发系统设计 -- 大文件业务

上传 分片断点秒传&#xff08;判断文件哈希值&#xff09; 前端不断的发送请求&#xff0c;如果用户暂停上传的话&#xff0c;那么就是前端停止发送请求就可以了。我分片了&#xff0c;而且记录了分片的相关信息&#xff0c;所以实现了断点功能。 前端把文件进行分片&#…

ftp vsftp 登录

打开windows资管管理器&#xff08;文件夹&#xff09;输入目标路径&#xff0c;如&#xff1a;ftp://192.168.1.1输入账号密码。 删除用户已保存的密码&#xff08;仅密码&#xff0c;名称记录还在&#xff09; 两种方法都可以试试&#xff0c;适用不同情况 情况-方法一&am…

Set、Map、类数组,傻傻区分不清楚?

前言 大家都知道&#xff0c;数组和对象是两种不同的数据结构&#xff0c;虽说在js数据类型中都属于Object&#xff0c;但是还是有一定的区别&#xff0c;通过字面量以及isArray、instanceof等方法&#xff0c;我们很好区分这两者。由于使用场景的原因js中衍生了很多类似的数据…

基于java(springboot+mybatis)网上音乐商城设计和实现以及论文报告

基于java(springbootmybatis)网上音乐商城设计和实现以及论文报告 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言…

Spring Boot 热部署

Spring Boot 热部署一、添加热部署框架支持二、Settings 开启项目自动编译三、开启运行中热部署四、使用 Debug 启动 (非 Run)一、添加热部署框架支持 或者右击鼠标添加依赖&#xff1a; 或者使用插件&#xff1a; 二、Settings 开启项目自动编译 三、开启运行中热部署 老版…

【数据库概论】第一章 绪论

第一章 绪论 1.1 数据库系统概述 数据库的四个基本概念 1.数据 数据是数据库中存储的基本对象&#xff0c;一般数据是描述事物的符号记录&#xff0c;这种符号记录可以输数字&#xff0c;也可以是文字、徒刑、音频等。 2.数据库 数据库是长期存储在计算机内有组织的&…

Leetcode动态规划题解

第一题 509. 斐波那契数 题目描述&#xff1a;斐波那契数&#xff08;通常用 F(n) 表示&#xff09;形成的序列称为斐波那契数列 。该数列由 0 和 1 开始&#xff0c;后面的每一项数字都是前面两项数字的和。也就是&#xff1a; F(0) 0&#xff0c;F(1) 1 F(n) F(n - 1) …

【计算机网络】计算机网络基础

计算机是人类社会不可或缺的工具&#xff0c;而单独的一台计算机的功能也是有限的&#xff0c;计算机需要和其它的设备相互连接通信形成的计算机网络才能对人类发展带来巨大的影响。 目录 计算机网络 通信协议 网络结构 网络边缘 接入网 网络核心 时延和吞吐量 时延 吞…

.Net Core6.0项目发布在IIS上访问404的问题

ASP.Net Core6.0项目发布在IIS上访问404的问题 进入线程池画面&#xff0c;将当前程序的线程池设为“无托管代码” 修改配置文件 Web.config&#xff0c;以下缺一不可 需要引用架包&#xff1a;Swashbuckle.AspNetCore.SwaggerUI.NetCore 6.0 自带集成了Swagger , 在发布项目时…