⌈算法进阶⌋图论::并查集——快速理解到熟练运用

目录

 一、原理

1. 初始化Init

  2. 查询 find 

3. 合并 union

二、代码模板

三、练习

1、  990.等式方程的可满足性🟢

2、  1061. 按字典序排列最小的等效字符串🟢

3、721.账户合并 🟡

4、  839.相似字符串组🟡

5、  2812.找出最安全路径 🔴


 一、原理

并查集主要运用与求一些不相交且有关联的集合的合并,这一点我们从后面的例题中进一步理解,我们首先掌握并查集的原理和运用

并查集的主要操作有:

1. 初始化Init

我们将每个数据看作一个树的节点,初始化每个节点指针指向自己

我们用一个数组fa[]的下标index表示节点, 用fa[index]表示该节点的根节点;

  2. 查询 find 

查询一个节点的根节点,以用于其他操作;

如上图, 若要查询数据3所在的集合的根节点,想做图这样的链接方式每次查询需要O(n)的时间,我们需要在查询时将树的结构转换成右图所示,这样之后的每次查询时间复杂度为O(logn)

3. 合并 union

实现两个集合的合并,可以抽象成两颗树的合并,即将两颗树的根节点相连;

二、代码模板

//初始化
vector<int> fa(n* n);
iota(fa.begin(), fa.end(), 0);
 

 //查询
function<int(int)> find = [&](int x) -> int { return x == fa[x] ? x : fa[x] = find(fa[x]); };  


 //合并
fa[find(x)] = find(y);   

三、练习

1、  990.等式方程的可满足性🟢

给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。

只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。 

解题思路:合并等式方程两侧字母,运用并查集管理相等字母集合

class Solution {
public:
    bool equationsPossible(vector<string>& equations) {
        //初始化
        vector<int> fa(26);
        iota(fa.begin(), fa.end(), 0);
        //查询
        function<int(int)> find = [&](int x) -> int { return fa[x] == x ? x : fa[x] = find(fa[x]); };

        for (auto s : equations) {
            if (s[1] == '=') {
                int x = s[0] - 97, y = s[3] - 97;
                fa[find(x)] = find(y);   //相等则合并
            }
        }
        for (auto s : equations) {
            int x = s[0] - 97, y = s[3] - 97;
            if (s[1] == '!' && find(x) == find(y)) return false;   //矛盾,返回false
        }
        return true;
    }
};

2、  1061. 按字典序排列最小的等效字符串🟢

给出长度相同的两个字符串s1 和 s2 ,还有一个字符串 baseStr 。

其中  s1[i] 和 s2[i]  是一组等价字符。

  • 举个例子,如果 s1 = "abc" 且 s2 = "cde",那么就有 'a' == 'c', 'b' == 'd', 'c' == 'e'

等价字符遵循任何等价关系的一般规则:

  •  自反性 'a' == 'a'
  •  对称性 'a' == 'b' 则必定有 'b' == 'a'
  •  传递性 :'a' == 'b' 且 'b' == 'c' 就表明 'a' == 'c'

例如, s1 = "abc" 和 s2 = "cde" 的等价信息和之前的例子一样,那么 baseStr = "eed" , "acd" 或 "aab",这三个字符串都是等价的,而 "aab" 是 baseStr 的按字典序最小的等价字符串

利用 s1 和 s2 的等价信息,找出并返回 baseStr 的按字典序排列最小的等价字符串。

 解题思路:由等价关系可以一眼看出这道题使用并查集,s1与s2对应字母放入一个集合(即将其合并),最终找到baseStr每个字母对应的集合中的最小字典序字母

class Solution {
public:
    string smallestEquivalentString(string s1, string s2, string baseStr) {
        //初始化
        vector<int> fa(26);
        iota(fa.begin(), fa.end(), 0);
        //查询
        function<int(int)> find = [&](int x) -> int { return fa[x] == x ? x : fa[x] = find(fa[x]); };

        for (int i = 0; i < s1.size(); i++) {
            int x = s1[i] - 97, y = s2[i] - 97;
            fa[find(y)] = find(x);   //合并
        }

        unordered_map<int, set<char>> g;   //统计以根节点代表的一个集合中的所有节点
        for (int i = 0; i < 26; i++) {
            g[find(i)].insert(i + 97);
        }
        
        for (int i = 0; i < baseStr.size(); i++) {
            //利用set默认排升序的特点,找到baseStr[i]的根节点的集合中的最小字母
            baseStr[i] = *(g[find(baseStr[i] - 97)].begin());   
        }
        return baseStr;
    }   
};

3、721.账户合并 🟡

给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是 名称 (name),其余元素是 emails 表示该账户的邮箱地址。

现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。

合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是 按字符 ASCII 顺序排列 的邮箱地址。账户本身可以以 任意顺序 返回。

解题思路:根据题意,拥有相同邮箱的账户为同一人,最终需要将相同用户的邮箱合并到一起,同样也是一眼并查集,但是代码实现起来可能有些复杂;首先需要遍历所有邮箱,判断是否有重复,将拥有同一邮箱的用户合并,需要特别注意的是,用户名相同不代表是同一人,最终将属于同一集合的用户邮箱用set去重,返回结果;

class Solution {
public:
    vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
        int n = accounts.size();
        //以accounts的下标[0, n)作为用户名字,方便以下标寻找父亲
        vector<int> fa(n);
        iota(fa.begin(), fa.end(), 0);    //初始化
        function<int(int)> find = [&](int i) -> int { return fa[i] == i ? i : fa[i] = find(fa[i]); };
        //key:邮箱  val:名字
        unordered_map<string, vector<int>> email_name;
        for (int i = 0; i < n; i++) {
            for (int j = 1; j < accounts[i].size(); j++) {    //遍历每个账户的所有邮箱
                email_name[accounts[i][j]].push_back(i);
                //如果同一个邮箱出现多个名字,那么认为为同一个人,此时连接
                if (email_name[accounts[i][j]].size() > 1) {
                    fa[find(email_name[accounts[i][j]][0])] = find(i);   //合并
                }
            }
        }
        
        //之前尝试用map,但是同一个名字可能不是同一人,所以map行不通
        //用set去重
        vector<set<string>> g(n);
        for (int i = 0; i < n; i++) {
            int f = find(i);   //找到根节点,将邮箱插入到根节点的数组中
            for (int j = 1; j < accounts[i].size(); j++) {
                g[f].insert(accounts[i][j]);    
            }
        }
        vector<vector<string>> ans;
        for (int i = 0; i < n; i++) {
            if (g[i].size() != 0) {    
                vector<string> tmp = {g[i].begin(), g[i].end()};  //将set转vector
                tmp.insert(tmp.begin(), accounts[i][0]);   //头插名字
                ans.push_back(tmp);
            }
        }
        return ans;
    }
};

4、  839.相似字符串组🟡

如果交换字符串 X 中的两个不同位置的字母,使得它和字符串 Y 相等,那么称 X 和 Y 两个字符串相似。如果这两个字符串本身是相等的,那它们也是相似的。

例如,"tars" 和 "rats" 是相似的 (交换 0 与 2 的位置); "rats" 和 "arts" 也是相似的,但是 "star" 不与 "tars""rats",或 "arts" 相似。

总之,它们通过相似性形成了两个关联组:{"tars", "rats", "arts"} 和 {"star"}。注意,"tars" 和 "arts" 是在同一组中,即使它们并不相似。形式上,对每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。

给你一个字符串列表 strs。列表中的每个字符串都是 strs 中其它所有字符串的一个字母异位词。请问 strs 中有多少个相似字符串组?

解题思路:暴力遍历+dfs查询相似字符串,将相似字符串合并,最终返回集合个数

class Solution {
public:
    int numSimilarGroups(vector<string>& strs) {
        int n = strs.size();
        //初始化
        vector<int> fa(n);
        iota(fa.begin(), fa.end(), 0);
        //查询
        function<int(int)> find = [&](int x) -> int { return x == fa[x] ? x : fa[x] = find(fa[x]); };   
        int vis[n];
        memset(vis, 0, sizeof(vis));
        //dfs
        function<void(int)> is_same = [&](int i) -> void {
            vis[i] = true;
            string& s1 = strs[i];
            for (int j = 0; j < n; j++) {
                if (!vis[j]) {
                    string& s2 = strs[j];
                    if (s1 == s2) {    //相同字符串相似,直接合并
                        fa[find(j)] = fa[i];
                        is_same(j);
                    } else {
                        int dif = 0;
                        for (int k = 0; k < s1.size(); k++) {
                            if (s1[k] != s2[k]) dif++;
                            if (dif > 2) break;
                        }
                        if (dif == 2) {         //恰有两个位置字符不同则相似,合并
                            fa[find(j)] = fa[i];
                            is_same(j);
                        }
                    }
                }
            }
        };
        //dfs入口
        for (int i = 0; i < n; i++) {
            if(!vis[i]) is_same(i);
        }
        //用set去重,返回集合个数
        set<int> ans;
        for (int i = 0; i < n; i++) ans.insert(find(i));   //将根节点插入set中
        return ans.size();
    }
};

5、  2812.找出最安全路径 🔴

给你一个下标从 0 开始、大小为 n x n 的二维矩阵 grid ,其中 (r, c) 表示:

  • 如果 grid[r][c] = 1 ,则表示一个存在小偷的单元格
  • 如果 grid[r][c] = 0 ,则表示一个空单元格

你最开始位于单元格 (0, 0) 。在一步移动中,你可以移动到矩阵中的任一相邻单元格,包括存在小偷的单元格。

矩阵中路径的 安全系数 定义为:从路径中任一单元格到矩阵中任一小偷所在单元格的 最小 曼哈顿距离。

返回所有通向单元格 (n - 1, n - 1) 的路径中的 最大安全系数 。

单元格 (r, c) 的某个 相邻 单元格,是指在矩阵中存在的 (r, c + 1)(r, c - 1)(r + 1, c) 和 (r - 1, c) 之一。

两个单元格 (a, b) 和 (x, y) 之间的 曼哈顿距离 等于 | a - x | + | b - y | ,其中 |val| 表示 val 的绝对值。

解题思路:由题,求从左上角到右下角路径中的最小安全系数的最大值:倒序枚举答案(安全系数), 将>=当前安全系数的坐标用并查集相连(合并),一次循环结束判断左上角与右下角是否相连

class Solution {
public:
    static constexpr int dirs[5] = {1, 0, -1, 0, 1};
    int maximumSafenessFactor(vector<vector<int>>& grid) {
        int n = grid.size();
        vector<pair<int, int>> q;
        vector<vector<int>> dis(n, vector<int>(n, -1));
        for (int i = 0; i < n; i++) {
            for (int j = 0;j < n; j++) {
                if(grid[i][j] == 1) {
                    q.push_back({i, j});
                    dis[i][j] = 0;
                }
               
            }
        }
        //bfs求每个点安全系数,及不同安全系数的值的下标(用于后续并查集的合并)
        vector<vector<pair<int, int>>> groups = {q};
        while (!q.empty()) {
            int safe = groups.size();
            vector<pair<int, int>> tmp;
            for (auto &[i, j] : q) {
                for (int k = 0; k < 4; k++) {
                    int x = i + dirs[k], y = j + dirs[k + 1];
                    if (x >= 0 && x < n && y >= 0 && y < n && dis[x][y] < 0) {
                        dis[x][y] = safe;
                        tmp.push_back({x, y});
                    }
                }
            }
            if (tmp.size() > 0)groups.push_back(tmp);
            q = move(tmp);
        }
        //初始化
        vector<int> fa(n*n);
        for (int i = 0; i < n * n; i++) fa[i] = i;
        //查询
        function<int(int)> find = [&](int x) -> int { return x == fa[x] ? x : fa[x] = find(fa[x]); };
        for (int ans = groups.size() - 1; ans > 0; ans--) {
            for (auto& [i, j] : groups[ans]) {
                for (int k = 0; k < 4; k++) {
                    int x = i + dirs[k], y = j + dirs[k + 1];
                    if (x >= 0 && x < n && y >= 0 && y < n && dis[x][y] >= dis[i][j]) 
                        fa[find(x * n + y)] = find(i * n + j);  //合并
                }
            }
            if (find(0) == find((n - 1) * n + n - 1)) return ans;   //相通则返回答案
        }
        return 0;
    }
};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【小程序】Canvas 画布分享海报

成品效果图 可以通过切换下面图片形成不同的海报背景分享图 <template><view>// type"2d"必须加<canvas type"2d" :style"{width:Artwidth px,height:Artheight px, margin:0 auto}" canvas-id"firstCanvas"id&quo…

uniapp调查问卷评价功能

我本来用的是uniapp官方提供的组件uni-rate组件&#xff0c;但修改成我想要的样式有点麻烦&#xff0c;于是我就自己手写一个&#xff0c;比用组件简单一点&#xff1b; dom结构 <text class"formTit must">请您对本次活动进行评价</text> <view cl…

【C语言】小游戏-三字棋

大家好&#xff0c;我是深鱼~ 目录 一、游戏介绍 二、文件分装 三、代码实现步骤 1.制作简易游戏菜单 2.初始化棋盘 3.打印棋盘 4.玩家下棋 5.电脑随机下棋 6.判断输赢 7.判断棋盘是否满了 四、完整代码 game.h(相关函数的声明&#xff0c;整个代码要引用的头文件以及宏…

探索未来:直播实时美颜SDK在增强现实(AR)直播中的前景

在AR直播中&#xff0c;观众可以与虚拟元素实时互动&#xff0c;为用户带来更加丰富、沉浸式的体验。那么&#xff0c;直播美颜SDK在AR中有哪些应用呢&#xff1f;下文小编将于大家一同探讨美颜SDK与AR有哪些关联。 一、AR直播与直播实时美颜SDK的结合 增强现实技术在直播中…

AtcoderABC222场

A - Four DigitsA - Four Digits 题目大意 给定一个整数N&#xff0c;其范围在0到9999之间&#xff08;包含边界&#xff09;。在将N转换为四位数的字符串后&#xff0c;输出它。如果N的位数不足四位&#xff0c;则在前面添加必要数量的零。 思路分析 可以使用输出流的格式设…

数据结构刷题训练——链表篇(三)

目录 文章目录 前言 1. 题目一&#xff1a;环形链表Ⅱ 1.1 思路 1.2 分析 1.3 题解 1.4 方法二 2. 题目二&#xff1a;复制带随机指针的链表 2.1 思路 2.2 分析 2.3 题解 总结 前言 在这个专栏博客中&#xff0c;我们将提供丰富的题目资源和解题思路&#xff0c;帮助读者逐步提…

Java多线程(2)---线程控制和线程安全的详细讲解

目录 前言 一.线程控制方法 1.1启动线程--start() 1.2线程睡眠---sleep()方法 1.3中断线程--interrupt() 方法 1.4等待线程---join() 二.线程安全 2.1数据不安全---数据共享 ⭐不安全的演示和原因 ⭐不安全的处理方法 ⭐synchronized的使用 2.2数据不安全---内存可…

数据结构刷题训练:用栈实现队列(力扣OJ)

目录 前言 1. 题目&#xff1a;用栈实现队列 2. 思路 3. 分析 3.1 定义 “ 队列 ” 3.2 创建队列 3.3 入队 3.4 队头数据 3.5 出队 3.6 判空和销毁 4.题解 总结 前言 栈和队列是数据结构中的两个重要概念&#xff0c;它们在算法和程序设计中都有着广泛的应用。本文将带你深入了…

4.时间与窗口

4.1 时间类型 在Flink中定义了3种时间类型&#xff1a; 事件时间&#xff08;Event Time&#xff09;:事件的发生事件&#xff0c;数据本身自带时间字段。处理时间&#xff08;Processing Time&#xff09;&#xff1a;计算引擎处理时的系统时间。和摄取时间&#xff08;Inge…

(el-Form)操作(不使用 ts):Element-plus 中 Form 表单组件校验规则等的使用

Ⅰ、Element-plus 提供的 Form 表单组件与想要目标情况的对比&#xff1a; 1、Element-plus 提供 Form 表单组件情况&#xff1a; 其一、Element-plus 自提供的 Form 代码情况为(示例的代码)&#xff1a; // Element-plus 自提供的代码&#xff1a; // 此时是使用了 ts 语言环…

ELK中grok插件、mutate插件、multiline插件、date插件的相关配置

目录 一、grok 正则捕获插件 自定义表达式调用 二、mutate 数据修改插件 示例&#xff1a; ●将字段old_field重命名为new_field ●添加字段 ●将字段删除 ●将filedName1字段数据类型转换成string类型&#xff0c;filedName2字段数据类型转换成float类型 ●将filedNam…

【移动机器人运动规划】04 ——轨迹生成

文章目录 前言相关代码整理: 介绍Minimum Snap OptimizationDifferential Flatness(微分平坦)Minimum-snapSmooth 1D TrajectorySmooth Multi-Segment TrajectoryOptimization-based Trajectory Generation Convex Optimization&#xff08;凸优化&#xff09;凸函数和凸集凸优…

List list=new ArrayList()抛出的ArrayIndexOutOfBoundsException异常

1.应用场景&#xff0c;今天生产日志监控到一组new ArrayList() 进行add 异常&#xff0c;具体日志如下&#xff1a; eptionHandler.handler(178): TXXYBUSSINESS|执行异常 java.util.concurrent.CompletionException: java.lang.ArrayIndexOutOfBoundsException: Index 1 out…

SpringBoot禁用Swagger3

Swagger3默认是启用的&#xff0c;即引入包就启用。 <dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version> </dependency> <dependency><groupId…

理解-面向对象

目录 对象&#xff1a; 举例&#xff1a; 封装: 好处: 继承: 多态&#xff1a; 类和对象之间的关系 对象&#xff1a; 把一个东西看成对象&#xff0c;我们就可以孤立的审查它的性质&#xff0c;行为&#xff0c;进而研究它和其他对象的关系。 对象是一个应用系统中用…

Spring5 AOP 默认使用 JDK

这是博主在使用dubbo实现远程过程调用的时候遇到的问题&#xff1a; 我们如果在服务提供者类上加入Transactional事务控制注解后&#xff0c;服务就发布不成功了。原因是事务控制的底层原理是为服务提供者类创建代理对象&#xff0c;而默认情况下Spring是基于JDK动态代理方式创…

ssh-keygen 做好免密登录后不生效

免密说明 通常情况下&#xff0c;我们ssh到其他服务器需要知道服务器的用户名和密码。对于需要经常登录的服务器每次都输入密码比较麻烦&#xff0c;因此我们可以在两台服务器上做免密登录&#xff0c;即在A服务器可以免密登录B服务器。 在A服务器上登录B服务器时&#xff0c;…

数字图像处理 --- 相机的内参与外参(CV学习笔记)

Pinhole Camera Model&#xff08;针孔相机模型&#xff09; 针孔相机是一种没有镜头、只有一个小光圈的简单相机。 光线穿过光圈并在相机的另一侧呈现倒立的图像。为了建模方便&#xff0c;我们可以把物理成像平面(image plane)上的图像移到实际场景(3D object)和焦点(focal p…

Spring-2-透彻理解Spring 注解方式创建Bean--IOC

今日目标 学习使用XML配置第三方Bean 掌握纯注解开发定义Bean对象 掌握纯注解开发IOC模式 1. 第三方资源配置管理 说明&#xff1a;以管理DataSource连接池对象为例讲解第三方资源配置管理 1.1 XML管理Druid连接池(第三方Bean)对象【重点】 数据库准备 -- 创建数据库 create …

Python基础小项目

今天给大家写一期特别基础的Python小项目&#xff0c;欢迎大家支持&#xff0c;并给出自己的完善修改 &#xff08;因为我写的都是很基础的&#xff0c;运行速率不是很好的 目录 1. 地铁票价题目程序源码运行截图 2. 购物车题目程序源码运行截图 3. 名片管理器题目程序源码运行…