AcWing:并查集
创始人
2024-05-29 03:08:44
0

并查集理论基础

并查集的作用是什么:

  1. 将两个集合合并。

  1. 询问两个元素是否在一个集合当中。

如果不使用并查集,要完成上述两个操作,我们需要:

创建一个数组来表示某个元素在某个集合之中,如belong[x] = a,即x元素在a集合之中。

那么完成第二个操作“询问两个元素是否在同一个集合”的时间复杂度为O(1),我们只需:

判断if(belong[x] == belong[y])

但是将两个集合合并的效率取决于两个集合中较短的集合的长度,时间复杂度较大O(len):

len = min(a.size(), b.size()); 再改变belong数组的值。

并查集的作用是,使用近乎O(1)的时间复杂度完成上述两个操作。

并查集基本原理:

每一个集合用一棵树来表示,树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点。

如何判断一个节点是不是树根呢?等价于if(p[x] == x)

如何求x的集合编号?while(p[x] != x) x = p[x];

如何合并两个集合?只需在两个集合中的任意一个加一条边即可,即:假设px是x的集合编号,py是y的集合编号,令p[x] = y即可。

这么看求x的集合编号那一步的时间复杂度还是挺高的,如何优化呢?

从x向上一旦找到根节点后,就将寻找路径上的每一个节点都直接指向根节点,这样这条路径上的所有节点在这之后的“求集合编号”的操作的时间复杂度就为O(1)了。于是并查集在经过优化后的时间复杂度就近似于O(1)了(路径压缩)。

AcWing 836. 合并集合

代码实现

定义一个p[N]数组来记录每个节点的父节点是谁。初始时刻每个元素单独成一个集合,其树根就是自己

#include using namespace std;const int N = 100010;
int p[N];
int n, m;// 路径压缩
int find(int x){if(p[x] != x) p[x] = find(p[x]);return p[x];
}int main(){scanf("%d %d", &n, &m);// initialfor(int i = 1; i <= n; i++){p[i] = i;}while(m--){char operate[2];int a, b;scanf("%s %d %d", operate, &a, &b);if(operate[0] == 'M'){p[find(a)] = find(b);}else{if(find(a) == find(b)) puts("Yes");else puts("No");}}return 0;
}

AcWing 837. 连通块中点的数量

在连通块中连边的操作,就相当于将连通块合并。

#include using namespace std;const int N = 100010;
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(){scanf("%d %d", &n, &m);for(int i = 1; i <= n; i++){p[i] = i;cnt[i] = 1;}while(m--){string operate;int a, b;cin >> operate;if(operate == "C"){cin >> a >> b;a = find(a), b = find(b);if(a == b) continue;cnt[b] += cnt[a];p[a] = b;}else if(operate == "Q1"){cin >> a >> b;if(find(a) == find(b)) puts("Yes");else puts("No");}else{cin >> a;cout << cnt[find(a)] << endl;}}return 0;
}

注意我开始定义了全局变量count[N]来记录连通块中点的数量,报错:

a.cpp:18:9: error: reference to 'count' is ambiguous18 |         count[i] = 1;|         ^~~~~

因为c++的库函数有关键字count,所以会冲突了,模糊不清。改成int cnt[N]后问题解决。

还有一个细节需要注意:

在C操作时,先把a,b的根结点取出来了:a = find(a), b = find(b);,因此接下来是先将集合a接到集合b下再把a的连通块大小加到b上,还是先把a的连通块大小加到b上再操作集合都是可以的,如果大家没有提前一步的处理,就必须要先加连通块大小再操作集合,否则操作完集合后,a和b的根结点将会重叠,导致输出错误。如下:

// accepted
if(operate == "C"){cin >> a >> b;if(find(a) == find(b)) continue;cnt[find(b)] += cnt[find(a)];p[find(a)] = find(b);
}// wrong
if(operate == "C"){cin >> a >> b;if(find(a) == find(b)) continue;p[find(a)] = find(b);cnt[find(b)] += cnt[find(a)];
}

AcWing 240. 食物链

并查集可以用来维护很多额外信息,如上一题,维护了每一个连接块的大小。

本题并查集维护一个距离数组d[N]来描述某节点到其父节点的距离,而距离用来表示食物链中的关系。

由于一共只有三类动物A,B,C,其中A吃B,B吃C,C吃A。这里假设根节点是某一种动物a1,离它距离为1的节点代表一种吃a1的动物a2,离根节点距离为2的节点代表一种吃a2的动物a3,离根节点距离为3的节点代表一种吃a3的动物a4(注意这里a4和a1是同一种动物)。

所以对于d[N]中维护的值来说,任意两个模3同余的值所代表的节点都表示同一种动物。

#include using namespace std;const int N = 50010;
int p[N], d[N];
int n, k;int find(int x){if(p[x] != x){// 存一下find(p[x])的值,因为如果这里直接p[x] = find(p[x])的话,// p[x]就变成根节点了,下面更新d[x]的语句就失效了int t = find(p[x]);d[x] += d[p[x]];p[x] = t;}return p[x];
}int main(){scanf("%d %d", &n, &k);for(int i = 1; i <= n; i++){p[i] = i;}int res = 0;while(k--){int t, x, y;scanf("%d %d %d", &t, &x, &y);// 当前的话中x或y比n大,为假if(x > n || y > n) res++;else{int px = find(x), py = find(y);if(t == 1){// x和y是同类且与前面的话冲突时,为假if(px == py && (d[x] - d[y]) % 3 != 0) res++;else if(px != py){p[px] = py;d[px] = d[y] - d[x];}}else{// x吃y且与前面的话冲突时,为假if(px == py && (d[x] - d[y] - 1) % 3 != 0) res++;else if(px != py){p[px] = py;d[px] = d[y] + 1 - d[x];}}}}printf("%d", res);return 0;
}

注意这里的 d[x] += d[p[x]]里的d[p[x]]是p[x]到它的父节点的距离。原本d[x]存的也是x到父节点的距离,然后p[x]最后变成根节点 ,d[x]才成了x到根节点的距离。

上面可能会带来疑问,为什么我们只在px == py时才进行res的判断?在px != py时只是将两个集合进行合并操作呢?

因为如果x,y动物不属于同一个集合,那么无论说什么,我们都认为这句话是真的:x,y不属于同一集合有两种可能,一种是x,y其中之一或者两者都是新出现的编号,那么新的编号没有和别的编号构成逻辑,直接联系起来即可。第二种是x,y都是之前出现过的编号,并且属于不同的集合, 这两个集合内部的逻辑关系(吃与被吃)与另一个集合没有任何关系,直接联系起来不会影响整体的逻辑关系

一个细节:

x、y经过运算后的d[x]、d[y]可能为负值,因为当 px != py时, d[x] 和 d[y] 的大小关系是不确定的,因此对 d[px] 的处理可能造成结果为负,后续在再次调用 find(x) 时可能会使 d[x] 为负数。所以这里的判断需要特别注意:

(d[x] - d[y]) % 3 != 0
为什么不能写成:
d[x] % 3 != d[y] % 3

原因就在上面提到,c++中负数模正数还是负数,就拿 -1 % 3 这个例子来说:

   -1 % 3 = (-1) - 3 * (-1 / 3)

在数学中和在计算机中虽然他们的计算过程相同,但是计算结果却有些差异。

所以要用第二种写法的话,要写成:

// wrong
d[x] % 3 != d[y] % 3
// right
(d[x] % 3 + 3) % 3 != (d[y] % 3 + 3) % 3

AcWing 1249. 亲戚

找亲戚,假设两个人互为亲戚,那么这两个人的亲戚都互为亲戚。本题也是一个并查集的应用,它涉及到了集合的合并,并询问两个点是不是属于同一个集合。

#include using namespace std;const int N = 200010;
int n, m, q;
int p[N];int find(int x){if(p[x] != x) p[x] = find(p[x]);return p[x];
}int main(){scanf("%d %d", &n, &m);for(int i = 1; i <= n; i++){p[i] = i;}while(m--){int a, b;scanf("%d %d", &a, &b);p[find(a)] = find(b);}scanf("%d", &q);while(q--){int c, d;scanf("%d %d", &c, &d);if(find(c) == find(d))  puts("Yes");else puts("No");}return 0;
}

这题不能用cin cout输入输出不然会超时。

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...