【算法简述】基础数据结构-并查集
并查集也是用来维护集合的,和前面学习的 set 不同之处在于,并查集能很方便地同时维护很多集 合。如果用 set 来维护会非常的麻烦。并查集的核心思想是记录每个结点的父亲结点是哪个结点。然而可以知道,这样空间时间复杂度极高,无法通过题目的时限.
并查集基本
基本结构
初始化
初始化很简单,将每个点所在集合初始化为它自己就可以了。
``` void init() { for (int i = 1; i <= n; ++i) { fa[i] = i; } } ```
查找
这一步,我们只需要找到根节点,即元素所在的集合,然后继续判断。
``` int get(int x) { if (fa[x] == x) { // x 结点就是根结点 return x; } return get(fa[x]); // 返回父结点的根结点 } ```
合并
将两个不同元素所在的集合合并为一个集合。
``` void merge(int x, int y) { x = get(x); y = get(y); if (x != y) { // 不在同一个集合 fa[y] = x; } } ```
实现并查集
首先,初始化
```
int fa[1000]; int n, m;
```
我们先把并查集的框架实现好,便于后面直接调用。初始化实际上就是把每个点的父亲结点赋值为自己。
void init() { for (int i = 1; i <= n; i++) { fa[i] = i; } }
我们继续实现get函数
```
int get(int x) { if (fa[x] == x) { return x; } return get(fa[x]); }
```
接下里我们实现 merge 函数,合并两个结点到一个集合。
```
void merge(int x,int y) { x=get(x); y=get(y); if (x!=y) { fa[y]=x; } }
```
初始化要在 输入之后,这是平时写程序很容易错误的一个点。
```
cin >> n >> m; init(); for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; merge(a, b); }
```
最后我们统计几何的个数。
```
int cnt = 0; for (int i = 1; i <= n; i++) { if (fa[i] == i) { cnt++; } } cout << cnt << endl;
```
实现争取代码:
```
#include<bits/stdc++.h> using namespace std; int fa[10000]; int n,m; void init() { for (int i = 1; i <= n; i++) { fa[i] = i; } } int get(int x) { if (fa[x] == x) { return x; } return get(fa[x]); } void merge(int x, int y) { x = get(x); y = get(y); if (x != y) { fa[y] = x; } } int main() { cin >> n >> m; init(); for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; merge(a, b); } int cnt = 0; for (int i = 1; i <= n; i++) { if (fa[i] == i) { cnt++; } } cout << cnt << endl; return 0; }
```
路径压缩
并查集的时间复杂度很高,最坏可以达到O(n),所以,我们更适用于路劲压缩的并查集,这一回,就来给大家讲一讲路劲压缩后的并查集。
路径压缩 的思想是,我们只关心每个结点所在集合的根结点,而并不太关心树的真正的结构是怎么样 的。这样我们在一次查询的时候,可以直接把查询路径上的所有结点的 都赋值成为根结点。实现 这一步只需要在我们之前的查询函数上面进行很小的改动。
联通性
之前我们判断无向图的连通性的时候(有向图的连通性我们会在之后的课程介绍),用的是 DFS 的方 法。而用并查集来判断无向图的连通性,只需要把每一条边连接的两个点合并到一个集合就可以了,都 不必存图,最后如果集合个数为 ,说明整个图是连通的。
列题解析
【模板】并查集
题目描述
如题,现在有一个并查集,你需要完成合并和查询操作。
输入格式
第一行包含两个整数N、M,表示共有N个元素和M个操作。
接下来M行,每行包含三个整数Zi、Xi、Yi
当Zi=1时,将Xi与Yi所在的集合合并
当Zi=2时,输出Xi与Yi是否在同一集合内,是的话输出Y;否则话输出N
输出格式
如上,对于每一个Zi=2的操作,都有一行输出,每行包含一个大写字母,为Y或者N
输入输出样例
输入 #1复制
4 7
2 1 2
1 1 2
2 1 2
1 3 4
2 1 4
1 2 3
2 1 4
输出 #1复制
N
Y
N
Y
说明/提示
时空限制:1000ms,128M
数据规模:
对于30%的数据,N<=10,M<=20;
对于70%的数据,N<=100,M<=1000;
对于100%的数据,N<=10000,M<=200000。
> 在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。
```
#include<bits/stdc++.h>
using namespace std;
const int maxn=100000;
int n,m;
int fa[maxn];
void init(){
for(int i=1;i<=n;i++){
fa[i]=i;
}
}
int get(int x){
if(fa[x]==x){
return x;
}
int r=get(fa[x]);
return fa[x]=r;
}
void cmp(int x,int y){
fa[get(x)]=get(y);
}
int main(){
cin>>n>>m;
init();
int a,b,c;
for(int i=1;i<=m;i++){
cin>>a>>b>>c;
if(a==1){
fa[get(b)]=get(c);
}else if(get(b)==get(c)){
cout<<"Y"<<endl;
}else{
cout<<"N"<<endl;
}
}
return 0;
}
```
亲戚
题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。
输入格式
第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。
以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有亲戚关系。
接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。
输出格式
P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。
输入输出样例
输入 #1复制
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
输出 #1复制
Yes
Yes
No
说明/提示
非常简单的并查集入门题哦!!!
> 这道题不就是并查集的模板题吗?
```
#include<bits/stdc++.h>
using namespace std;
const int maxn=100000;
int n,m,p;
int fa[maxn];
void init(){
for(int i=1;i<=n;i++){
fa[i]=i;
}
}
int get(int x){
if(fa[x]==x){
return x;
}
return get(fa[x]);
}
void cmp(int x,int y){
fa[get(x)]=get(y);
}
int main(){
cin>>n>>m>>p;
init();
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
if(get(x)!=get(y)){
cmp(x,y);
}
}
for(int i=1;i<=p;i++){
int x,y;
cin>>x>>y;
if(fa[get(x)]==get(y)){
fa[i]=i;
}
}
for(int i=1;i<=p;i++){
if(fa[i]){
cout<<"Yes"<<endl;
}else{
cout<<"No"<<endl;
}
}
return 0;
}
```
网络交友
网络交友
描述
在网络社交的过程中,通过朋友,也能认识新的朋友。在某个朋友关系图中,假定 A 和 B 是朋友,B 和 C 是朋友,那么 A 和 C 也会成为朋友。即,我们规定朋友的朋友也是朋友。
现在要求你每当有一对新的朋友认识的时候,你需要计算两人的朋友圈合并以后的大小。
输入
第一行:一个整数 n(n\leq 5000)n(n≤5000),表示有 nn 对朋友认识。
接下来 nn 行:每行输入两个名字。表示新认识的两人的名字,用空格隔开。(名字是一个首字母大写后面全是小写字母且长度不超过 20 的串)。
输出
对于每一对新认识的朋友,输出合并以后的朋友圈的大小。
输入样例 1
3
Fred Barney
Barney Betty
Betty Wilma
输出样例 1
2
3
4
> 提示
>
> 提示:用map把字符串映射成为数字,然后并查集来维护集合大小。
```
#include<bits/stdc++.h>
using namespace std;
map<string,string> father;
map<string,int> sizes;
string get(string x)
{
if( father[x] == x )
{
return x;
}
return get( father[x] );
}
void merge (string x,string y){
string a , b;
a = get(x);
b = get(y);
if( a != b ){
father[a] = b;
sizes[b] += sizes[a];
}
cout<<sizes[b]<<endl;
}
int main(){
int n,i,id=1;
string a,b;
cin>>n;
for(i=0;i<n;++i){
cin>>a>>b;
if(father[a]==""){
father[a] = a;
sizes[a] = 1;
}
if(father[b]==""){
father[b] = b;
sizes[b] = 1;
}
merge(a,b);
}
return 0;
}
```
关押罪犯
描述
S 城现有两座监狱,一共关押着 NN 名罪犯,编号分别为 11 ~ NN。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。我们用 “怨气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之间的积怨越多。如果两名怨气值为 cc 的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并造成影响力为 cc 的冲突事件。
每年年末,警察局会将本年内监狱中的所有冲突事件按影响力从大到小排成一个列表,然后上报到 S 城 Z 市长那里。公务繁忙的 Z 市长只会去看列表中的第一个事件的影响力,如果影响很坏,他就会考虑撤换警察局长。
在详细考察了 NN 名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。
那么,应如何分配罪犯,才能使 Z 市长看到的那个冲突事件的影响力最小?这个最小值是多少?
输入
输入第一行为两个正整数 N(1 \le N \le 20000)N(1≤N≤20000) 和 M(1 \le M \le 100000)M(1≤M≤100000),分别表示罪犯的数目以及存在仇恨的罪犯对数。
接下来的 MM 行每行为三个正整数 a_j,b_j,c_ja
j
,b
j
,c
j
,表示 a_ja
j
号和 b_jb
j
号罪犯之间存在仇恨,其怨气值为 c_jc
j
。数据保证 1<a_j, b_j \le N1<a
j
,b
j
≤N,0 < c_j \le 10^90<c
j
≤10
9
,且每对罪犯组合只出现一次。
输出
输出共一行,为 Z 市长看到的那个冲突事件的影响力。如果本年内监狱中未发生任何冲突事件,请输出 00。
输入样例 1
4 6
1 4 2534
2 3 3512
1 2 28351
1 3 6618
2 4 1805
3 4 12884
输出样例 1
3512
提示
> 样例解释: 罪犯之间的怨气值如下面左图所示,右图所示为罪犯的分配方法,市长看到的冲突事件影响力是 35123512(由 22 号和 33
> 号罪犯引发)。其他任何分法都不会比这个分法更优。
>
> 提示: 二分最大的怨气值为 xx。对于怨气值小于等于 xx 的组合,分不分在一个监狱都没有影响了。对于怨气值大于 xx
> 的组合,我们就不能让他们在一个监狱,实际上就是一个二分图判断,可以直接用 DFS 来标记即可。
>
> 当然这题还可以用并查集来做。
来源
NOIP2010
```
#include<bits/stdc++.h>
using namespace std;
const int maxn=200010;
int n,m;
int maze[maxn];
struct node{
int x,y,z;
}a[maxn];
int cmp(node a,node b){
return a.z>b.z;
}
void init(){
for(int i=1;i<=n*2;i++){
maze[i]=i;
}
}
int get(int x){
if(maze[x]!=x)maze[x]=get(maze[x]);
return maze[x];
}
void merge(int x,int y){
x=get(x);
y=get(y);
maze[x]=y;
}
int main(){
cin>>n>>m;
init();
for(int i=1;i<=m;i++){
cin>>a[i].x>>a[i].y>>a[i].z;
}
sort(a+1,a+1+m,cmp);
for(int i=1;i<=m;i++){
merge(a[i].x,a[i].y+n);
merge(a[i].x+n,a[i].y);
if(get(a[i].x)==get(a[i].y)){
cout<<a[i].z<<endl;
return 0;
}
}
cout<<0;
return 0;
}
```
接龙
接龙
描述
蒜头君在玩一种接龙的游戏,蒜头君有 3000030000 张卡片分别放在 3000030000 列,每列依次编号为 1,2,...,300001,2,...,30000。同时,蒜头君也把每张卡片依次编号为 1,2,...,300001,2,...,30000。
游戏开始,蒜头君让让第 ii 张卡片处于第 i(i = 1,2,...,30000)i(i=1,2,...,30000) 列。然后蒜头君会发出多次指令,每次调动指令 MM ii jj 会将第 ii 张卡片所在的队列的所有卡片,作为一个整体(头在前尾在后)接至第 jj 张卡片所在的队列的尾部。
蒜头君还会查看当前的情况,发出 CC ii jj 的指令,即询问电脑,第 ii 张卡片与第 jj 张卡片当前是否在同一个队列中,如果在同一列中,那么它们之间一共有多少张卡片。
聪明的你能不能编写程序处理蒜头君的指令,以及回答蒜头君的询问呢?
输入
第一行有一个整数 TT(1\leq T\leq 5000001≤T≤500000),表示总共有 TT 条指令。
以下有 TT 行,每行有一条指令。指令有两种格式:
MM ii jj:ii 和 jj 是两个整数(1\leq i, j\leq 300001≤i,j≤30000),表示指令涉及的卡片编号。你需要让第 ii 张卡片所在的队列的所有卡片,作为一个整体(头在前尾在后)接至第 jj 张卡片所在的队列的尾部,输入保证第 ii 号卡片与第 jj 号卡片不在同一列。
CC ii jj:ii 和 jj 是两个整数(1\leq i, j\leq 300001≤i,j≤30000),表示指令涉及的卡片编号。该指令是蒜头君的询问指令。
输出
如果是蒜头君调动指令,则表示卡片排列发生了变化,你的程序要注意到这一点,但是不要输出任何信息;
如果是蒜头君的询问指令,你的程序要输出一行,仅包含一个整数,表示在同一列上,第 ii 号卡片与第 jj 号卡片之间的卡片数目(不包括第 ii 张卡片和第 jj 张卡片)。如果第 ii 号卡片与第 jj 号卡片当前不在同一个队列种中,则输出 -1−1。
输入样例 1
4
M 2 3
C 1 2
M 2 4
C 4 2
输出样例 1
-1
1
> 提示
>
> 提示
>
> 我们可以很容易的统计俩张卡片是否在同一个队列中,用并查集就可以了。关键是怎么计算,在一个队列中的俩个卡片之间卡片数目,只要维护一下每个卡片到队列头的卡片数目就好了。
>
> 在计算同一队列中的俩个卡片之间卡片数目,只要把俩个卡片到队列头的卡片数目做差就可以了。
```
#include <iostream>
#include <cmath>
using namespace std;
const int N=3e4+9;
int n,m;
int fa[N];
int size[N],dist[N];
void init(){
for(int i=0;i<N;i++){
fa[i]=i;
size[i]=1;
dist[i]=0;
}
}
int get(int x){
if(fa[x]==x){
return x;
}
int y=fa[x];
fa[x]=get(y);
dist[x]+=dist[y];
return fa[x];
}
void merge(int x,int y){
x=get(x);
y=get(y);
if(x!=y){
fa[x]=y;
dist[x]=size[y];
size[y]+=size[x];
}
}
int main(){
init();
int t;
cin>>t;
while(t--){
char op;
int x,y;
cin>>op>>x>>y;
if(op=='M'){
merge(x,y);
}else{
if(get(x)==get(y)){
cout<<fabs(dist[x]-dist[y])-1<<endl;
}else{
cout<<"-1"<<endl;
}
}
}
return 0;
}
```