本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的。同时也希望文章能够让你有所收获,与君共勉!

今天来看看并查集,顾名思义,并查集的本质就是一个集合,支持快速合并集合,时间复杂度为\(\,O(1)\),以及查找某一元素属于某个集合,时间复杂度近乎为\(\,!O(1)\)(路径压缩后的时间复杂度),除此之外,并查集还有一大优势就是支持定义额外数组来存储额外的信息,如定义数组d表示当前元素到根节点的路径长度,形成带权重的并查集,增加数组size来记录每个集合内有多少个元素,这也是后面这两道模板题所需要维护的数组。

那就先通过这道题来认识一下并查集吧!

连通块中点的数量

给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。

现在要进行 m 个操作,操作共有三种:

C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;

Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;

Q2 a,询问点 a 所在连通块中点的数量;

输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 C a bQ1 a bQ2 a 中的一种。

输出格式

对于每个询问指令 Q1 a b,如果 a 和 b 在同一个连通块中,则输出 Yes,否则输出 No。

对于每个询问指令Q2 a,输出一个整数表示点 a 所在连通块中点的数量

每个结果占一行。

数据范围

1≤n,m≤105

输入样例:

5 5

C 1 2

Q1 1 2

Q2 1

C 2 5

Q2 5

输出样例:

Yes

2

3

算法原理

并查集的建立很简单,就是定义数组fa(Father),fa[i]表示数字i的父节点为fa[i],使用fa[i] = i进行赋值,即每个数字的父节点最开始都是他自己,也就是每个数字代表的集合只有它自己一个元素,这个集合的祖宗节点也是他自己。

路径压缩

并查集最重要的操作就是寻找集合的祖宗节点。这里我们把并查集想象成一棵树,我们要做的就是如何找到这个树的根节点。

这个操作的主要思想就是不断访问节点的父节点看他什么时候等于x本身,如果fa[x] == x,则找到该集合的祖宗节点,并将p[x](此时p[x]与x相同)返回,如果找不到则不断搜索父节点,这个过程的时间复杂度往往与并查集的深度有关,在数据量较大时时间复杂度为\(O(n)\),那么我们就可以使用路径压缩去优化查找祖宗节点,优化后查找的时间复杂度为\(O(1)\),那么该怎么优化呢?

我们可以在找到根节点后,将其赋值给查找路径上的每一个结点,即fa[x] = find(fa[x]),具体的实现其实就是在递归的回归过程中把找到的祖宗节点依次赋值给路径上的每个结点,让这些路径结点的父节点直接指向祖宗节点,这时fa[x]就是祖宗节点,而不是原来的那个父节点了,也就是变成了一个深度为一层的N叉树,如图所示。

合并两个并查集

合并两个并查集可以看成是合并了两棵树,那么树该怎么合并呢,很容易就可以想到,只要让其中一个集合的父节点是另一个结合就可以啦,如对于ab两个集合的祖宗节点,那么fa[a] = b就代表合并两个集合,也就是将集合a指向集合b的祖宗节点。

查找连通块中元素的个数

实际上就是维护了一个size数组,size[i]表示第i个集合中存在的元素个数,那么如何维护size数组呢,我们只需要初始化size中元素全为1,在合并两个并查集时顺便把集合a中元素的个数size[a]加到集合bsize[b]中就行了,即size[b] += size[a]

代码实现

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1000010;
int p[N],cnt[N];
int n,m; int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
} int main(){
cin >> n >> m;
for(int i=1 ; i<=n ; ++i) {
p[i]=i;
cnt[i] = 1;
}
while(m--){
string s;
int a,b;
cin >> s ;
if(s == "Q1"){
cin >> a >> b;
if(find(a) == find(b)){
cout << "Yes" << endl;
}
else{
cout << "No" << endl;
}
}
else if(s == "Q2"){
cin >> a;
cout << cnt[find(a)] << endl;
}
else if(s == "C"){
cin >> a >> b;
a = find(a),b = find(b);
if(a!=b){
p[a] = b;
cnt[b] += cnt[a]; // 用+=会先find(a)再find(b),这样b的位置会错
}
}
}
return 0;
}

接下来看一道关于并查集应用的题,认真理解哦

食物链

动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。

A 吃 B,B 吃 C,C 吃 A。

现有 N 个动物,以 1∼N 编号。

每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N 个动物所构成的食物链关系进行描述:

第一种说法是 1 X Y,表示 X 和 Y 是同类。

第二种说法是 2 X Y,表示 X 吃 Y。

此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

当前的话与前面的某些真的话冲突,就是假话;

当前的话中 X 或 Y 比 N 大,就是假话;

当前的话表示 X 吃 X,就是假话。

你的任务是根据给定的 N 和 K 句话,输出假话的总数。

输入格式

第一行是两个整数 N 和 K,以一个空格分隔。

以下 K 行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中 D 表示说法的种类。

若 D=1,则表示 X 和 Y 是同类。

若 D=2,则表示 X 吃 Y。

输出格式

只有一个整数,表示假话的数目。

数据范围

1≤N≤50000,

0≤K≤100000

输入样例:

100 7

1 101 1

2 1 2

2 2 3

2 3 3

1 1 3

2 3 1

1 5 5

输出样例:

3

算法原理

主要思路

第一个难点:如何建立吃与被吃的关系

还是先来理解一下题意吧,A吃B,B吃C,C吃A,如图



容易知道当存在a吃b,而b吃c时,我们就能推断出c吃a。当先出现a吃b时,就不会再出现b吃a。那么我们就可以找到如图这样的关系

我们可以维护一个表示结点到根节点的距离的数组d把食物链的传递关系抽象成路径长度,假设存在一个三层的树,若某一结点到根节点的路径距离为1,则说明这个结点可以吃根节点,距离根节点长度为2的结点,说明这个结点可以吃距离为1的结点,同时也可以被根节点吃(也就对应了距离为3的结点),那么距离根节点长度为3的结点不就是根节点的同类嘛,它是可以吃距离为2的结点,这样就把食物链的传递关系转化为路径长度的循环,每3个结点就是一轮循环,即(d[x]-d[y])%3就表示x和y之间的关系,0为同类,1为x吃y,2为y吃x

那么为什么会这样呢,我们可以把%3给放进括号里,变成d[x]%3-d[y]%3d[x]%3即x与根节点的距离模3,d[y]%3同理,你看这不就跟上面的循环对上了吗?当(d[x]%3-d[y]%3) = 0时,即(d[x] - d[y])%3 == 0也就是x与y距离祖宗节点的距离之差为0,x与y是同类,当(d[x]%3-d[y]%3) = 1时,即(d[x]-d[y])%3 == 1也就是x与y距离祖宗节点的距离之差为1,x的父亲是y,即x吃y,那么当(d[x]%3-d[y]%3) = 2,即(d[x] - d[y])%3 == 2也就是x与y距离祖宗节点的距离之差为2,x的父节点的父节点才是y,说明x与根节点是同类,y吃x。也就是说只要将结点加入到并查集中就建立了结点与这个集合所有节点的关系,我们就能通过分别比较他们与根节点的距离之差来得到他们之间的关系,因此属于同一个集合里面所有的结点关系都是可以推断出的。后面更多是从代数的角度逆推为什么要这么做的,而另一篇大佬写的博客(地址在这)是从向量运算的角度写的,让人耳目一新(虽然我看不懂)。


第二个难点:分辨清什么时候话为真,什么时候话为假

接下来就让我们看看这道题的具体求解思路是什么?读入说法t,与ab两个动物,很明显对于规则中的第二条和第三条,我们可以分别使用if(a>n || b>n)if(t == 2 && a == b)进行表示,最后就剩下当前的话与之前的真话是否冲突,那么之前的真话都有什么呢?这就分为两类,一类是XY是同类,另一类是XY

我们通过并查集实现,谁第一个出现关系谁就是真话,每个动物编号第一次出现时都属于新的集合,这时只能合并集合建立联系,第一次建立关系的一定是真话,第二个是x,y都是之前出现过的编号并且属于不同集合的可以直接建立联系,也就是两个集合并没有合并,一个集合内部的关系不会联系另一个集合,这时合并两个集合也是第一次建立联系,也就说明这句话是真话。

对于第一种冲突,如何辨别他们在并查集中是同类呢,我们只需要知道他们到祖宗节点的距离模3后的值相同就可以认为他们是同类(注意ab的祖宗结点必须用同一个祖宗节点才能作为参考点,而具有同一个祖宗节点的前提就是他们都是出现过的,区别只在于是不是处于同一个集合,也就是祖宗节点是否相同),即(d[a]-d[b])%3 == 0,对于不符合(d[a]-d[b])%3 == 0这个条件的,说明ab就不是同类,那么对于不具有同一个祖宗节点的动物ab来说,他们是要么有只出现过一次的动物,要么就是出现过但是不在同一个集合,但不管哪一个他们两个之间都没有建立联系,这时我们只需要合并两个集合p[px] = py,将a的祖宗结点连接到b的祖宗结点上,这时px就不再是x的祖宗结点,自然而然的我们就要维护px到祖宗节点的距离d[px] = d[y] - d[x],至于为什么是这个样子,把d[x]移到左边就知道了。

对于第二种冲突,我们该如何在并查集中探究到底是谁吃谁呢?这时我们很容易的就知道,当他们是同一个祖宗节点的两个动物a,b时,我们可以通过(d[a] - d[b] - 1) % 3来表示ab(看不懂的可以自己推一下),当然也可以用(d[a] - d[b] - 2)% 3来表示ab吃。对于不是同一个祖宗节点的,那就说明a,b一定有一个是第一次出现的动物,这时我们需要将其加入到同一个并查集中去,即p[px] = py,并且维护距离d[px] = d[y] - d[x] + 1来表示xy的关系,至于为什么是这个样子,把d[x]移到左边就知道了(注意:这里的+1就表示x吃y,他们到根节点的距离之差为1)。

一些实现主要思路的补充

主要的思路都讲完了,只剩下关于在查找祖宗节点时需要维护那些关系的操作了,在这个过程中需要实现两个操作,一个就是路径压缩p[x] = find(p[x]),这里就不提了,另一个就是维护距离信息d,在搜索过程中我们需要得到这个动物距离祖宗节点的距离d[x],这里使用了动态规划的思想得到的递推式,d[x] = d[x] + d[p[x]],即当前这个结点到祖宗节点的距离就是这个结点目前的状态加上父结点到祖宗节点的距离,当某个动物第一次进入集合时x=px=p[x]d[px] = d[y] - d[x] + 1就让d[x]的初始状态为1,因此d[x]直接+d[p[x]]就可以直接得到x到根节点的距离d[x]最后一定记得要路径压缩哟,因为之前的操作都是不改变父节点p[x]的情况下进行的,即并没有进行路径压缩,不然会使得父节点直接变成祖宗节点的

代码实现

#include <iostream>

using namespace std;

const int N = 50010;

int n,m;

int p[N],d[N];
int res = 0; int find(int x)
{
if (p[x] != x)
{
int t = find(p[x]);
d[x] += d[p[x]];
p[x] = t;
}
return p[x];
} int main()
{
scanf("%d%d",&n,&m);
for(int i=1 ; i<=n ; ++i) p[i] = i;
while(m--){
int t,x,y;
scanf("%d%d%d",&t,&x,&y);
if(x>n || y>n) res++;
else{
int px = find(x),py = find(y);
if(t == 1){
if(px == py && (d[x]-d[y])%3) res++; // 不是同类的
else{ // 出现过并且属于不同的集合没建立联系
p[ px ] = py; // 把px连到py后面
d[ px ] = d[y] - d[x]; // x与y是同类,所以把d[x]移到左边就能理解了
}
}
else{
// 说明a吃b
// 先判断是否为假话,并且已经出现过
if(px == py && (d[x]-d[y]-1)%3) res++; // a不吃b时d[x]-d[y]1结果不为0,b吃a,说明为假话,res++
else{
// 没有出现过a或b,这里第一次出现,说明为真话,或者出现过但不是同一个集合里的,但不管哪一个他们都属于不同集合,这是第一次描述这个关系,是真话
p[ px ] = py; // 把px连到py后面
d[ px ] = d[y] + 1 - d[x]; // x吃y把d[x]移到左边就得到祖宗节点的值应该是多少
}
}
}
}
cout << res << endl;
return 0;
}

难难难!太难了,实在是有点抽象,把人都写自闭了,这篇博客是我边思考边记的,所以难免会有些词不达意,说活啰里啰唆的问题,希望大佬们能够谅解,如果有意思表达不清或出现错误的地方也欢迎留言进行交流,及时改正吧

2022-11-14 Acwing每日一题的更多相关文章

  1. CISP/CISA 每日一题 14

    CISA 每日一题(答) 自动无人值守运行(LIGHTS-OUT)优势:1.信息系统运行成本的遏制/减少:2.持续运行(24/7):3.减少系统错误和中断次数. I/O 控制人员负责保证:1.批处理信 ...

  2. CISP/CISA 每日一题 11

    CISA 每日一题(答) 一个合理建造的数据仓库应当支持下列三种基本的查询格式: 1.向上溯源和向下溯源——向上溯源是对数据进行总计:向下溯源是将数据进行细化: 2.交叉溯源——通过通用属性访问数据仓 ...

  3. 老男孩IT教育-每日一题汇总

    老男孩IT教育-每日一题汇总 第几天 第几周 日期 快速访问链接 第123天 第二十五周 2017年8月25日 出现Swap file….already exists以下错误如何解决? 第122天 2 ...

  4. CISP/CISA 每日一题 五

    CISA 每日一题(答) 信息系统审计师要确认系统变更程序中的: 1.变更需求应有授权.优先排序及跟踪机制: 2.日常工作手册中,明确指出紧急变更程序: 3.变更控制程序应同时为用户及项目开发组认可: ...

  5. [每日一题]面试官问:谈谈你对ES6的proxy的理解?

    [每日一题]面试官问:谈谈你对ES6的proxy的理解? 关注「松宝写代码」,精选好文,每日一题 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...

  6. 【python】Leetcode每日一题-寻找旋转排序数组中的最小元素

    [python]Leetcode每日一题-寻找旋转排序数组中的最小元素 [题目描述] 已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组.例如,原数组nums ...

  7. 【JavaScript】【dp】Leetcode每日一题-解码方法

    [JavaScript]Leetcode每日一题-解码方法 [题目描述] 一条包含字母 A-Z 的消息通过以下映射进行了 编码 : 'A' -> 1 'B' -> 2 ... 'Z' -& ...

  8. Java实习生常规技术面试题每日十题Java基础(八)

    目录 1.解释内存中的栈(stack).堆(heap)和静态区(static area)的用法. 2.怎样将GB2312编码的字符串转换为ISO-8859-1编码的字符串? 3.运行时异常与受检异常有 ...

  9. PL/SQL Challenge 每日一题:2014-3-14 11gR2中带RELIES_ON子句的RESULT_CACHE函数

    PL/SQL Challenge 每日一题:2014-3-14 11gR2中带RELIES_ON子句的RESULT_CACHE函数 最先答对且答案未经编辑的puber将获得纪念章一枚(答案不可编辑但可 ...

  10. JZOJ 11.14 提高B组反思

    JZOJ 11.14 提高B组反思 T1 题目虽然有点高大上,但是很容易懂 有一个\(d\)维空间,同时有一个长度为\(2n\)的操作序列,每个操作往某一维的正方向或反方向走一格,问多少种方案使得最后 ...

随机推荐

  1. 关于 Math.random()生成指定范围内的随机数的公式推导

    关于 Math.random()生成指定范围内的随机数的公式推导 在 java 中,用于生成随机数的 Math 方法 random()只能生成 0-1 之间的随机数,而对于生成指定区间,例如 a-b ...

  2. Spring_事务总结

    Spring 事务总结 rollbackFor 设为 Exception.class场景下 如果在函数内部catch住异常消费掉,没有再抛出的话,不会回滚 如果catch住 然后原封不动抛出,会回滚 ...

  3. mysql_varchar与中英文关系总结

    mysql 4.0版本以下,varchar(50), 指的是50字节,如果存放utf8汉字时,只能存放16个(每个汉字3字节) mysql 5.0版本以上,varchar(50), 指的是50字符,无 ...

  4. [Python]-sklearn.model_selection模块-处理数据集

    拆分数据集train&test from sklearn.model_selection import train_test_split 可以按比例拆分数据集,分为train和test x_t ...

  5. Prometheus Operator 对接 Thanos

    文章转载自:https://jishuin.proginn.com/p/763bfbd56ae4 使用 Prometheus Operator 来进行监控,在 Prometheus 高可用的章节中也手 ...

  6. kubeoperator 使用外部mysql

    1.导出 kubeoperator 的数据库 sql 文件,然后导入到外部mysql 2.正常关闭 kubeoperator 3.关闭 kubeoperator 不会影响已经部署的 k8s 集群 4. ...

  7. Elasticsearch:fielddata 介绍

    默认情况下,大多数字段都已编入索引,这使它们可搜索. 但是,脚本中的排序,聚合和访问字段值需要与搜索不同的访问模式. 搜索需要回答"哪个文档包含该术语?"这个问题,而排序和汇总则需 ...

  8. Kibana:在Kibana中对数据进行深入分析

  9. 《吐血整理》高级系列教程-吃透Fiddler抓包教程(24)-Fiddler如何优雅地在正式和测试环境之间来回切换-中篇

    1.简介 在开发或者测试的过程中,由于项目环境比较多,往往需要来来回回地反复切换,那么如何优雅地切换呢?宏哥今天介绍几种方法供小伙伴或者童鞋们进行参考. 2.实际工作场景 2.1问题场景 (1)已发布 ...

  10. 常量的定义(const和#define)

    定义常量的方法 //均要在调用前(区别全局变量!!) 1.使用#define预处理器 2.使用const关键字 1.#define #define 常量名 常量值 //定义形式,常量名不可以是数字开头 ...