并查集详解+Leecode相关题集(一)

总结一下最近遇到的 Union-Find 算法,也就是常说的并查集算法,主要是解决图论中「动态连通性」问题的,名词很高端,其实特别好理解。
关于什么是并查集以及并查集的详解,强烈推荐看这两篇博文,并查集详解(超级简单有趣~~就学会了)(这一篇讲的很通俗易懂,基本上看一遍就懂了)、LeetCode Union-Find(并查集) 专题(一)(这一篇讲的比较专业,还有例题可以看看)

本文主要是列举一些本人在刷leecode的时候遇见的一些可以用并查集做的题集。

为了不让文章篇幅过长,分为上下文两部分,上文主要包含了:
684. 冗余连接(还有冗余连接Ⅱ,由于差不多,未加进来,感兴趣的可以自行查看)、 737. Sentence Similarity II 句子相似度之二、 547. 朋友圈、990. 等式方程的可满足性。

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是输入数组的大小。

先明确几个概念
1.集合树:所有节点以代表节点为父节点构成的多叉树
2.节点的代表节点:可以理解为节点的父节点,从当前节点出发,可以向上找到的第一个节点
3.集合的代表节点:可以理解为根节点,意味着该集合内所有节点向上走,最终都能到达的节点
来个图帮助理解
在这里插入图片描述
上图中是一棵集合树,树中有1-6总计6个节点
整个集合的代表节点是1
4节点的代表节点是3,6节点的代表节点是1
无论沿着哪个节点向上走,最终都会达到集合代表节点的1节点
然后具体到这个题上:
我们以这个边集合为例子[[1,2], [3,4], [3,2], [1,4], [1,5]]

一、首先,对于边集合edges的每个元素,我们将其看作两个节点集合
比如边[2, 3],我们将其看作节点集合2,和节点集合3

二、在没有添加边的时候,各个节点集合独立,我们需要初始化各个节点集合的代表节点为其自身
所以,我们先初始化一个容器vector,使得vector[i]=i
这里两个i意思不同,作为索引的i是指当前节点,作为值的i是指当前节点所在集合的代表节点
比如vector[2] = 2,意味着2这个节点所在集合的代表节点就是2,没有添加边的情况下,所有节点单独成集合,自身就是代表节点
初始化后,集合图如下图所示:
在这里插入图片描述
三、然后我们开始遍历边集合,将边转化为集合的关系
这里有一点很重要:边[a,b]意味着a所在集合可以和b所在集合合并。
合并方法很多,这里我们简单地将a集合的代表节点戳到b集合的代表节点上
这意味着,将b集合代表节点作为合并后大集合的代表节点
对于一个集合的代表节点s,一定有s->s,意思是s如果是代表节点,那么它本身不存在代表节点
假设我们的读取顺序为[[1,2], [3,4], [3,2], [1,4], [1,5]]
初始化vector[0, 1, 2, 3, 4, 5]
对应的index [0, 1, 2, 3, 4, 5]
1.读取[1,2]:
读取顺序为[[1,2], [3,4], [3,2], [1,4], [1,5]]
当前vector[0, 1, 2, 3, 4, 5]
当前index [0, 1, 2, 3, 4, 5]
原本1->1,2->2,
由1节点出发,vector[1]=1, 找到1所在集合的代表节点1
由2节点出发,vector[2]=2, 找到2所在集合的代表节点2
于是,将1的代表置为2,vector[1]=2, vector[2]=2
对应的vector[0, 2, 2, 3, 4, 5]
对应的index [0, 1, 2, 3, 4, 5]
原集合变为下图:
在这里插入图片描述
########################################################
2.读取[3, 4]
读取顺序为[[1,2], [3,4], [3,2], [1,4], [1,5]]
当前vector[0, 2, 2, 3, 4, 5]
当前index [0, 1, 2, 3, 4, 5]
同理,将3所在集合的的代表节点3的代表节点置为4
对应的vector[0, 2, 2, 4, 4, 5]
对应的index [0, 1, 2, 3, 4, 5]
集合变化如下图:
在这里插入图片描述
3.读取[3, 2]
读取顺序为[[1,2], [3,4], [3,2], [1,4], [1,5]]
当前vector[0, 1, 2, 4, 4, 5]
当前index [0, 1, 2, 3, 4, 5]
从节点3出发,vector[3]=4, vector[4]=4,于是找到节点3所在集合的代表节点为4
从节点2出发,vector[2]=2, 找到节点2所在集合的代表节点为2
于是,将4的代表置为2,vector[4]=2, vector[2]=2
对应的vector[0, 2, 2, 4, 2, 5]
对应的index [0, 1, 2, 3, 4, 5]
集合变化如下图:
在这里插入图片描述
4.读取[1, 4]
读取顺序为[[1,2], [3,4], [3,2], [1,4], [1,5]]
当前vector[0, 2, 2, 4, 2, 5]
当前index [0, 1, 2, 3, 4, 5]
从节点1出发,vector[1]=2, vector[2]=2, 找到节点1所在集合代表节点为2
从节点4出发,vector[4]=2, vector[2]=2, 找到节点4所在集合代表节点为2
由于1和4的代表节点相同,说明这两个节点本身就在同一个集合中
由于原图是无向图,路径是双向可达的,1能够到达2,而且2能够到达4,再加上1能够到达4
说明1能通过两条路径到达4,,这也意味着这条边出现的时候,原图中一定出现了环
至于题中要求的,返回最后一条边,其实这就是返回添加过后会构成环的那一条边
直白解释就是,在这条边出现之前,图中没有环
这条边出现,图中也出现环。包括这条边在内,构成环的边都是满足破圈条件的边
然而谁是最后一条出现在边集合里的?当然,就是这条构成环的最后一条边
########################################################

到这里,对于此题的实现基本上说完了,直接上代码吧

class Solution {
public:
    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
        vector<int> pre(1001); //存放第i个元素的父节点
        int size = edges.size();
        // 初始化各元素为单独的集合,代表节点就是其本身
        for(int i=0;i<size;i++)
            pre[i] = i;
        for(int j = 0; j < size; j++){
            // 找到边上两个节点所在集合的代表节点
            int set1 = find(edges[j][0], pre);
            int set2 = find(edges[j][1], pre);
            if(set1 == set2)  // 两个集合代表节点相同,说明出现环,返回答案
                return edges[j];
            else    // 两个集合独立,合并集合。将前一个集合代表节点戳到后一个集合代表节点上
                pre[set1] = set2;
        }
        return {0,0};
    }
    int find(int n, vector<int>& pre){
        // int num = n;x
        while(pre[n] != n)
            n = pre[n];
            pre[n] = pre[pre[n]];
        return n;
    }
};

[LeetCode] 737. Sentence Similarity II 句子相似度之二(本题需要会员才可以做,我从别的地方找到了英文原题,然后翻译了一下)
题目如下:
给定两个句子:words1、words2(每个都表示为一个字符串数组)和一组相似的单词对,确定两个句子是否相似。
例如,单词1 = [“great”, “acting”," skills"]和单词2 = [“fine”, “drama”, “talent”]是相似的,如果相似的词组是成对的= [[“great”, “good”], [“fine”, “good”], [“acting”,“drama”], [“skills”,“talent”]]。

请注意,相似关系是可传递的。例如,如果“great”和“good”相似,“fine”和“good”相似,那么“great”和“fine”也相似。
相似也是对称的。例如,“great”和“fine”是相似的,就像“fine”和“great”是相似的。
而且,一个词总是和它自己相似。例如,句子words1 = [“great”], words2 = [“great”], pair =[]是相似的,尽管没有指定的相似的词对。
最后,句子只有在单词数量相同的情况下才可能相似。所以像words1 = [“great”]这样的句子永远不会和words2 = [“doubleplus”,“good”]相似。

注意:
单词1和单词2的长度不超过1000。
对的长度不超过2000。
每对[i]的长度为2。
每个单词[i]和对[i][j]的长度将在[1,20]范围内。

题解:
这道题题目大意是给定一个相似的单词对序列, 并且这种相似性有传递性(这句话是个很明显的提示, 需要进行合并操作)。 然后给出两个单词向量, 问是否这两个向量每对都是相似的。
基本思路: 根据相似单词对列表建立并查集,建立完成之后扫描两个单词向量, 看每一对是否都在一个相似域中。(即每一对单词的根是否一致)

这里有个小trick : 按照题目直接进行并查集建立, 需要一个map[string, string].但是, 如果预先把单词利用一个map[string, int]做一个映射, 那么 我们需要处理的又是一个最简单的并查集了。

class Solution {
public:
    bool areSentencesSimilarTwo(vector<string>& words1, vector<string>& words2, vector<pair<string, string>> pairs) {
        if (words1.size() != words2.size()) { return false; }
	unordered_map<string, string> parents;
		for (int i = 0; i < words1.size(); ++i)
			parents[words1[i]] = words1[i];
		for (int i = 0; i < words2.size(); ++i)
			parents[words2[i]] = words2[i];
		for (int i = 0; i < pairs.size(); ++i) {
			 string firstP = findParent(pairs[i].first, parents);
			 string secondP = findParent(pairs[i].second, parents);
			 parents[secondP] = firstP;
		}
		for (int i = 0; i < words1.size(); ++i) {
			if (findParent(words1[i], parents) != findParent(words2[i], parents)) 
	                return false;
	    }
	    return true;
    }
	string findParent(string son, unordered_map<string, string>& parents) {
		if (son == parents[son]) return son;
        parents[son] = findParent(parents[son], parents);
        return parents[son];
    }			

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。
注意:

N 在[1,200]的范围内。
对于所有学生,有M[i][i] = 1。
如果有M[i][j] = 1,则有M[j][i] = 1。

这道题没什么好说的,也是利用并查集来做
直接上代码:

class Solution {
public:
    int fa[300];  //father


    int find(int x){
        while(x != fa[x])
            x= fa[x];
        return x;
    }
    int findCircleNum(vector<vector<int>>& M) {
        if(M.size() == 0) return 0;
        int n = M.size();
        int ans=n;

        for(int i= 0;i < n;i++) fa[i]=i;
        for(int i = 0; i < n; i++)
            for(int j = 0; j < i; j++){
                if(M[i][j] == 0) continue;
                if(find(fa[i]) != find(fa[j])){
                    fa[find(i)] = fa[find(j)];
                    ans--;
                }
            }
        return  ans;
    }
};

990. 等式方程的可满足性

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

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

输入:[“a == b”,“b != a”]
输出:false
解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。
示例 2:

输出:[“b == a”,“a == b”]
输入:true
解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。
示例 3:

输入:[“a == b” , “b == c”,“a==c”]
输出:true
示例 4:

输入:[“a == b”,“b!=c”,“c == a”]
输出:false
示例 5:
输入:[“c == c”,“b == d”,“x!=z”]
输出:true

提示:
1 <= equations.length <= 500
equations[i].length == 4
equations[i][0] 和 equations[i][3] 是小写字母
equations[i][1] 要么是 ‘=’,要么是 ‘!’
equations[i][2] 是 ‘=’

题解:
思想:逐个“==”对字母union,然后检查“!=”的字母是否在同一集合!

class Solution {
public:
    int fa[30];  //二十六个字母
    bool equationsPossible(vector<string>& equations) {
        for(int i = 0; i < 26; i++) fa[i] = i;
        for(int i=0;i<equations.size();i++)
        {
            if(equations[i][1] == '=') //只取等式进行连通并集
            {
                int x = find(equations[i][0] - 'a');
                int y = find(equations[i][3] - 'a');
                fa[x] = y;  //合并集       
            }
        }
        for(auto eq : equations){
            if(eq[1] == '!'){
                int x = find(eq[0] - 'a');
                int y = find(eq[3] - 'a');
                if(x == y) return false;
            }
        }
        return true;
    }
    int find(int x){
        while(x != fa[x])
            x = fa[x];
        return x;
    }
};
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值