本系列所有题目均为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. 第八十四篇:Vue购物车(五) 商品数量的增减

    好家伙, 1.商品数量的增减 我们把商品的数量增减独立出来,写成一个独立的组件Counter <template> <div class="number-container ...

  2. Altium Designer 18学习

    目录 目录 快捷键 通孔 敷铜 修改铜皮与导线之间的间隔 去除指定敷铜区域 DRC设计规则检查问题: 快捷键 EJC 快速跳转到器件 M 移动 CTRL+M 测量距离 通孔 敷铜 放置多边形平面 -- ...

  3. mysql_阻塞和死锁

    什么是阻塞 由于不同锁之间的兼容关系,造成一个事务需要等待另一个事务释放其所占用的资源的现象 称为 阻塞 如何发现阻塞 mysql_8.0 SELECT waiting_pid as '被阻塞的线程' ...

  4. Dockerfile中ADD命令详细解读

    ADD指令的功能是将主机构建环境(上下文)目录中的文件和目录.以及一个URL标记的文件 拷贝到镜像中. 其格式是: ADD 源路径 目标路径 #test FROM ubuntu MAINTAINER ...

  5. Elasticsearch:单节点数据迁移

    Elasticsearch数据迁移:windows单节点迁移到windows 将源数据中的ES安装目录下的data/nodes目录整体拷贝到目标ES的对应目录下 迁移前请备份:迁移后需要重启ES: E ...

  6. 部署一个生产级别的 Kubernetes 应用(以Wordpress为例)

    文章转载自:https://mp.weixin.qq.com/s?__biz=MzU4MjQ0MTU4Ng==&mid=2247487811&idx=1&sn=67b39b73 ...

  7. git-flow模型

    git-flow 是在 git branch 和 git tag 基础上封装出来的代码分支管理模型,把实际开发模拟称 master develop feature release hotfix sup ...

  8. host主机监控规则

    1.先在 Prometheus 主程序目录下创建rules目录,然后在该目录下创建 host.yml文件,内容如下: 内容很多,可以根据实际情况进行调整. 规则参考网址:https://awesome ...

  9. Visual Studio 2022 开发 STM32 单片机 - 环境搭建点亮LED灯

    安装VS2022社区版软件 选择基础的功能就好 安装VisualGDB软件(CSDN资源) 按照提示一步一步安装就好 VisualGDB激活软件(CSDN资源) 将如下软件放在VisualGDB的安装 ...

  10. 「国产系统」Tubian 0.3,兼容Windows和Android的GNU/Linux系统!

    0.4版已发布:https://www.cnblogs.com/tubentubentu/p/16741197.html Sourceforge.net主页(提供下载):https://sourcef ...