并查集学习记录:模板/思路汇总

HB小咸鱼学习记录*

自我对于“并查集”的理解

有时候一些题,是让你判断图中一些数据是否在一个集合中。例如1和3联通,2和3联通,问你1和2是否联通。这其实问的就是1和2是否在一个联通集合里,如果用搜索进行遍历的话,就需要挨个对路径进行尝试,如果数据量大的话,消耗时间就会过多,这时候就可以用并查集来解决问题。

并查集的大致思路

并查集的核心操作就是“并”与“查”。
“并”指的是将两个数据放到一个集合里,“查”就是查询一个数据在哪个集合里。
首先,我们声明一个father数组,数组的值是指向当前下标元素的父节点。
其次,我们对这个数组进行初始化,使得当前下标的值是他本身。代表他是自己的父节点,即他是根节点,这个情况下可以看作每个元素都是一个单独的集合。

int father[MAX];
for(int now=1;now<MAX;now++)//初始化 
{
    
    
	father[now]=now;
}

接着,我们建立“查”操作,查询某一数据属于哪个集合,就是查询他的根节点。
因为我们设定了father数组,所以我们不断查找该数据的父节点,即可知道该数据的根节点。
我们如果要查询两个数据是否属于一个集合,即可通过“查”操作获取两个数据的根节点,如果两个数据的根节点相同,则说明两个数属于同一个集合。

//第一种写法(递归)
//第一种写法可以用三目运算符缩写为 return father[a]==a ? a : find(father[a]);
int find(int a)
{
    
    
	if(father[a]==a)
		return a;
	return find(father[a]);
}
//第二种写法
int find(int a)
{
    
    
	int temp = father[a];
	while(father[temp]!=temp)
		temp = father[temp];
	return temp;
}

最后,我们建立“并”操作,可以将两个集合合并。
我们首先获取想要合并的数据A和数据B的根节点。如果根节点相同,则说明两个数据本来就属于一个集合,所以不用进行合并处理;如果根节点不同,则说明两个数据不属于同一个集合,此时我们需要进行合并操作。
合并操作很简单,让一个数据的根节点指向另一个数据的根节点即可。

int union_(int a,int b)
{
    
    
	int temp_a=find(a);
	int temp_b=find(b);
	if(temp_a!=temp_b)
	{
    
    
		father[temp_a]=temp_b;	
	} 
}

一些优化

秩优化
我们在进行“并”操作时,如果和上面一样,规定无论如何都是数据A往数据B并,那么很有可能大树并小树,导致整个集合的深度增加,最极端的例子是形成了一条链,此时如果find链尾,则会将整个链遍历一遍,时间消耗会大大增加。所以我们在进行“并”操作时,可以获取两个数据所处集合的深度,让深度低的成为深度高的子集。而当深度一样时,则可以看你的喜好进行合并。

int deep[MAX] = {
    
    0};//深度数组,初始深度都为0,储存各个集合的深度 
int union_(int a,int b)
{
    
    
	int temp_a=find(a);
	int temp_b=find(b);
	if(temp_a!=temp_b)//两个数据不在同一个集合
	{
    
    
		if(deep[temp_a]>deep[temp_b])//a比b深度高 
		{
    
    
			father[temp_b]=temp_a;//b成为a的子集 
		}
		else if(deep[temp_a]<deep[temp_b])//a比b深度低 
		{
    
    
			father[temp_a]=temp_b;//a成为b的子集 
		}	
		else//深度一样 
		{
    
    
			father[temp_b]=temp_a;//b成为a的子集 
			deep[temp_a]++;//a的根节点的深度加一 
		}
	} 
}

路径压缩
在我们进行find查询时,如果我们只在乎某数据的根节点,而不在意他的各个父节点时,我们可以进行路径压缩。让这个数据的父节点直接指向根节点,这样被称作“路径压缩”。在进行路径压缩后,所有的节点都指向根节点,这样集合的深度只有1,在之后进行数据的根节点查询时的复杂度只有O(1),大大提升查询速度。

//第一种写法(递归)
//第一种写法可以用三目运算符缩写为 return father[a]==a ? a : father[a] = find(father[a]);
int find(int a)
{
    
    
	if(father[a]==a)
		return a;
	return father[a] = find(father[a]);
}
//第二种写法
int find(int a)   
{
    
    
    int temp = father[a];
	while(father[temp]!=temp)
		temp = father[temp];
    int i=a,j;
    while(i!=temp) //路径压缩
    {
    
    
         j=father[i]; // 在改变上级之前用临时变量j记录下他的值
         father[i]=temp; //把上级改为根节点
         i=j;
    }
    return temp ;
}

并查集的大致模板

这是秩优化+路径压缩的模板,
其余版本看上面的思路模块即可。

int father[MAX];//父节点数组
int deep[MAX] = {
    
    0};//深度数组,初始深度都为0,储存各个集合的深度 

void init()//初始化 
{
    
    
	for(int now=1;now<MAX;now++)
	{
    
    
		father[now]=now;
	}
}

int find(int a)//查询
{
    
    
	if(father[a]==a)
		return a;
	return father[a] = find(father[a]);
}

int union_(int a,int b)//合并
{
    
    
	int temp_a=find(a);
	int temp_b=find(b);
	if(temp_a!=temp_b)//两个数据不在同一个集合
	{
    
    
		if(deep[temp_a]>deep[temp_b])//a比b深度高 
		{
    
    
			father[temp_b]=temp_a;//b成为a的子集 
		}
		else if(deep[temp_a]<deep[temp_b])//a比b深度低 
		{
    
    
			father[temp_a]=temp_b;//a成为b的子集 
		}	
		else//深度一样 
		{
    
    
			father[temp_b]=temp_a;//b成为a的子集 
			deep[temp_a]++;//a的根节点的深度加一 
		}
	} 
}

int main()
{
    
    
	........
	return 0;
}

并查集例题

1.畅通工程

某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?

Input
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。

Output
对每个测试用例,在1行里输出最少还需要建设的道路数目。

Sample Input
4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0

Sample Output
1
0
2
998

题目地址

思路:这题在把所有的数据接收后,运用并查集进行集合合并,最后根节点的个数就是集合的个数。而联通n个节点最少需要n-1条边,故根节点的个数减去1就是建设道路的最少值。由于题中只在乎最后根节点的个数,所有使用了路径压缩,提高代码的运算速度。

AC代码:

#include<bits/stdc++.h>
using namespace std;

int father[1001];

int find(int a)
{
    
    
	return father[a]==a ? a : father[a] = find(father[a]);
}
 
int compose(int a,int b)
{
    
    
	int temp_a=find(a);
	int temp_b=find(b);
	if(temp_a!=temp_b)
	{
    
    
		father[temp_a]=temp_b;	
	} 
}

int main()
{
    
    
	int a,b,c,d;
	
	while(scanf("%d",&a)&&a)
	{
    
    
		scanf("%d",&b);
		for(int now=1;now<=a;now++)//初始化 
		{
    
    
			father[now]=now;
		}
		
		while(b--)
		{
    
    
			scanf("%d %d",&c,&d);
			compose(c,d);
		}
		
		int ans=0;
		for(int now=1;now<=a;now++)
		{
    
    
			if(father[now]==now)
				ans++;
		}
		
		cout<<ans-1<<endl;
	} 
	
	return 0;
}

2.修改数组

给定一个长度为N 的数组A = [A1, A2,…,AN],数组中有可能有重复出现的整数。
现在小明要按以下方法将其修改为没有重复整数的数组。小明会依次修改A2,A3,…, AN。
当修改Ai 时,小明会检查Ai 是否在A1~ Ai-1 中出现过。
如果出现过,则小明会给Ai 加上1 ;
如果新的Ai 仍在之前出现过,小明会持续给Ai 加1 ,直到Ai 没有在A1~Ai-1中出现过。
当AN 也经过上述修改之后,显然A数组中就没有重复的整数了。
现在给定初始的A 数组,请你计算出最终的A 数组。

输入
第一行包含一个整数N(1<=N<=100000)
第二行包含N个整数A1,A2,…,AN(1<=Ai<=1000000)

输出
输出N个整数,依次是最终的A1,A2,…,AN

样例输入 Copy
5
2 1 1 3 4

样例输出 Copy
2 1 3 4 5

题目地址

思路:我们可以把用过的数字放到一个集合里,而让他的父节点指向下一个可以用的数字,具体操作就是father[a]=find(father[a]+1)。这样我们就会一直对使用过的数字的父节点进行加一操作,直到找到一个没有被使用过的数字。由于这题只需要知道该数字是否被使用过,即是否在“被使用过”这个集合里,所以我们可以使用路径压缩,提高运算效率。

AC代码:

#include<bits/stdc++.h>
using namespace std;

int father[1000001];

int find(int a)
{
    
    
	if(father[a]==-1)
	{
    
    
		father[a]=a;
		return a;
	}	
	return father[a]=find(father[a]+1);
}

int main()
{
    
    
	int input,aa;
	cin>>input;
	memset(father,-1,sizeof(father));
	for(int now=1;now<=input;now++)
	{
    
    
		cin>>aa;
		aa=find(aa);
		cout<<aa<<" ";
	}
	return 0;
}

3.敌人

俗话说得好,敌人的敌人就是朋友。
现在有n个人,编号1至n,初始互不相识。接下来有m个操作,操作分为两种:

(1)检查x号和y号是否是朋友,若不是,则变成敌人
(2)询问x号的朋友有多少个
请你针对每个操作中的询问给出回答。

输入
第一行两个正整数n、m,表示人的数量和操作的数量。
接下来m行,依次描述输入。每行的第一个整数为1或2表示操作种类。对于操作(1),随后有两个正整数x,y。对于操作(2),随后一个正整数x。

输出
输出包含m行,对于操作(1),输入’N’或"Y",'N’表示x和y之前不是朋友,'Y’表示是朋友。对于操作(2),输出x的朋友数量。

输入示例
5 8
1 1 2
1 1 3
1 2 3
2 3
1 4 5
2 3
1 1 4
2 3

输出示例
N
N
Y
1
N
1
N
2

思路:这道题相比之前的题,一个不同的特点就是我们无法直接将两个数据放进同一个集合,因为输入的数据要变为“敌人”关系,即不在同一个集合内。那么我们如何能将两个数据放进同一个集合呢?我们可以扩大father数组,使它是原来的两倍大。假如一共有N个数,则1到N代表本身,N+1到2N则代表1到N的敌人。当我们设定两个数是敌人的时候,只需要把第一个数据和第二个数据的敌人放在一个集合,第二个数据和第一个数据的敌人防在一个集合,即可完成合并的操作。因为当和一个数的敌人是朋友时,那和这个数就是敌人。当查询一个数的朋友时,遍历查询与其根节点相同的点的个数,再减去一(它本身),即为朋友的个数。本题由于也是只看根节点,所以可以使用路径压缩。

AC代码:

#include<bits/stdc++.h>
using namespace std;

int father[600001];//最大300000个数 数的敌人也有300000个 故一共600000

int find(int a)//查询
{
    
    
	return father[a]==a ? a : father[a] = find(father[a]);
}

int compose(int a,int b)//合并
{
    
    
	int temp_a=find(a);
	int temp_b=find(b);
	if(temp_a!=temp_b)
	{
    
    
		father[temp_a]=temp_b;
	}
}

int main()
{
    
    
	int input1,input2,a,b,c;
	cin>>input1>>input2;
	
	for(int now=1;now<=input1*2;now++)//初始化 
	{
    
    
		father[now]=now;
	}
	
	for(int now=0;now<input2;now++)//接收初始值 
	{
    
    
		cin>>a;
		if(a==1)//1操作
		{
    
    
			cin>>b>>c;
			if(find(b)==find(c))//是朋友
			{
    
    
				cout<<"Y"<<endl;
			}
			else
			{
    
    
				compose(b,c+input1);//将输入的第一个数据和第二个数据的敌人放在一个集合里
				compose(c,b+input1);//将输入的第二个数据和第一个数据的敌人放在一个集合里
				cout<<"N"<<endl;
			}
		}
		else if(a==2)//2操作
		{
    
    
			cin>>b;
			int ans=0;
			/*for(int now=1;now<=input1*2;now++) 
			{
				cout<<father[now]<<" ";
			}*/
			for(int now=1;now<=input1;now++)//如果根节点相同则说明在同一集合,是朋友 
			{
    
    
				if(find(b)==find(now))
					ans++;
			}
			cout<<ans-1<<endl;
		}
	}
	
	return 0;
}

4.食物链

有N只动物分别编号为1,2,……,N。所有动物都属于A,B,C中的一类。已知A能吃掉B,B能吃掉C,C能吃掉A。按顺序给出下面的两种信息共K条:
第一种:x和y属于同一类;
第二种:x吃y。
然而这些信息可能会出错,有可能有的信息和之前给出的信息矛盾,也有的信息可能给出的x和y不在1到N的范围内。求在K条信息中有多少条是不正确的。计算过程中,我们将忽视诸如此类的错误信息。

输入
第一行两个自然数,两数间用一个空格分隔,分别表示N和K,接下来的K行,每行有三个数,第一个数为0或1,分别对应第一种或第二种,接着的两个数,分别为该条信息的x和y,三个数两两之间用一个空格分隔。

输出
一个自然数,表示错误信息的条数。

输入示例
100 7
0 101 1
1 1 2
1 2 3
1 3 3
0 1 3
1 3 1
0 5 5

输出示例
3

思路:与上一题“敌人”相似,这一题也可以通过扩展数组建立多重关系来做。这一题存在同类、吃、被吃这三个关系。所以我们把数组扩到到原来的三倍,1到N代表本身,N+1到2N代表被“本身”吃的,2N+1到3N代表吃“本身”的。当我们建立0操作的“A和B同类”关系时,只需要把三个区域同等合并即可,即A本身和B本身合并为一类,A吃的和B吃的合并为一类,吃A的和吃B的合并为一类,即可说明A和B地位相同。当我们建立1操作的“A吃B”关系时,将B和A吃的划为一类,B吃的和吃A的划为一类,A和吃B的划为一类,即可实现A吃B关系网的建立。
而当输入超限、在0操作时判断出A和B是吃或被吃关系、在1操作时判断出A和B是同类或被吃关系时,即为语句错误,答案数量加一。由此结束时输出即可。

AC代码:

#include<bits/stdc++.h>
using namespace std;

int father[150001];

int find(int a)
{
    
    
	return father[a]==a ? a : father[a] = find(father[a]);
}

int compose(int a,int b)
{
    
    
	int temp_a=find(a);
	int temp_b=find(b);
	if(temp_a!=temp_b)
	{
    
    
		father[temp_a]=temp_b;
	}
}

int main()
{
    
    
	int input1,input2,a,b,c,ans=0;
	cin>>input1>>input2;
	
	for(int now=1;now<=input1*3;now++)//初始化 
	{
    
    
		father[now]=now;
	}
	
	for(int now=0;now<input2;now++)//接收初始值 
	{
    
    
		cin>>a;
		if(a==0)//输入0 x与y同类 
		{
    
    
			cin>>b>>c;
			if(b>input1||b<1||c>input1||c<1)//输入不合法 
			{
    
    
				ans++;
				continue;
			}
			else
			{
    
    
				if(find(b)==find(c+input1)||find(b)==find(c+2*input1))//如果属于吃或者被吃关系 
				{
    
    
					ans++;
					continue;
				}
				else//指向平级,代表同类 
				{
    
    
					compose(b,c); 
					compose(b+input1,c+input1);
					compose(b+input1*2,c+input1*2);
				}
			}
		}
		else if(a==1)//输入1 x吃y 
		{
    
    
			cin>>b>>c;
			if(b>input1||b<1||c>input1||c<1)//输入不合法 
			{
    
    
				ans++;
				continue;
			}
			else
			{
    
    
				if(find(b)==find(c)||find(b)==find(c+input1))//属于同一类或者被吃关系 
				{
    
    
					ans++;
					continue;
				}
				else
				{
    
    
					compose(b+input1,c);//b吃的 和 c一类 即 b吃c 
					compose(b+input1*2,c+input1);//吃b的 和 c吃的一类  
					compose(b,c+input1*2);//b 和 吃c的一类 
				}
			}
		}
	}
	
	cout<<ans<<endl;//结果
	
	return 0;
}

5. 网络分析

时间限制: 1.0s 内存限制: 256.0MB 本题总分:25 分
问题:
小明正在做一个网络实验。
他设置了 n 台电脑,称为节点,用于收发和存储数据。
初始时,所有节点都是独立的,不存在任何连接。
小明可以通过网线将两个节点连接起来,连接后两个节点就可以互相了。两个节点如果存在网线连接,称为相邻。
小明有时会测试当时的网络,他会在某个节点发送一条信息,信息会到每个相邻的节点,之后这些节点又会转发到自己相邻的节点,直到所有或间接相邻的节点都收到了信息。所有发送和接收的节点都会将信息存储一条信息只存储一次。
给出小明连接和测试的过程,请计算出每个节点存储信息的大小。

输入:
输入的第一行包含两个整数 n, m,分别表示节点数量和操作数量。节1 至 n 编号。
接下来 m 行,每行三个整数,表示一个操作。
如果操作为 1 a b,表示将节点 a 和节点 b 通过网线连接起来。当 时,表示连接了一个自环,对网络没有实质影响。
如果操作为 2 p t,表示在节点 p 上发送一条大小为 t 的信息。

输出:
输出一行,包含 n 个整数,相邻整数之间用一个空格分割,依次表示完上述操作后节点 1 至节点 n 上存储信息的大小。

样例输入:
4 8
1 1 2
2 1 10
2 3 5
1 4 1
2 2 2
1 1 2
1 2 4
2 2 1

样例输出:
13 13 5 3

评测用例规模与约定:
对于 30% 的评测用例,1 ≤ n ≤ 20,1 ≤ m ≤ 100。
对于 50% 的评测用例,1 ≤ n ≤ 100,1 ≤ m ≤ 1000。
对于 70% 的评测用例,1 ≤ n ≤ 1000,1 ≤ m ≤ 10000。
对于所有评测用例,1 ≤ n ≤ 10000,1 ≤ m ≤ 100000,1 ≤ t ≤ 100。

思路: 核心思路是并查集。一个数组用来存旧值,一个数组用来存根结点权值,一个数组是并查集的father数组。执行2操作时,查集寻找到操作值的根结点并加权。执行1操作时,若两数在一个集内则不操作;若两数不在一个集内,则对所有节点进行遍历,使他们的旧值数组加上其根节点的权值,随后进行并集操作。记得对权值数组进行清零,防止后面重复计算。

AC代码: OJ链接

#include<bits/stdc++.h>
using namespace std;

int old_[10001],new_[10001],father[10001],n,m;//存老值 存根节点新值 父节点数组 

int find(int a)
{
    
    
	return father[a]==a ? a : father[a]=find(father[a]);//路径压缩
}

void union_(int a,int b)
{
    
    
	int temp_a=find(a),temp_b=find(b);
	if(temp_a!=temp_b)
	{
    
    
		for(register int now=1;now<=n;now++)
		{
    
    
			old_[now]+=new_[find(now)];//旧值数组遍历加上权值
		}
		memset(new_,0,sizeof(new_));//重置权值数组,防止重复计算
		father[temp_a]=temp_b;
	}
}

int main()
{
    
    
	int a,b,c;
	memset(new_,0,sizeof(new_));
	for(register int now=1;now<=10000;now++)
	{
    
    
		father[now]=now;//father数组初始化 
	} 
	scanf("%d %d",&n,&m);
	while(m--)
	{
    
    
		scanf("%d %d %d",&a,&b,&c);
		if(a==1)
		{
    
    
			union_(b,c);
		}
		else
		{
    
    
			new_[find(b)]+=c;
		}
	} 
	for(register int now=1;now<=n;now++)
	{
    
    
		printf("%d ",old_[now]+new_[find(now)]);
	}
	return 0;
}

小结

在使用并查集中,要根据题目数据选择合适的优化。
一般都得用路径压缩提高效率,但是秩优化用的比较少(我感觉),因为在进行路径压缩后秩优化后的结构就不复存在的,我感觉二者是有点矛盾的。但是两者一起使用相较于只使用路径压缩也会在第一次接收数据时提高一点效率,但是为了敲代码的效率,我还是喜欢只敲路径压缩。
使用并查集时,要选择合适的数据结构
例如秩优化时的储存深度的数组、例题第三题“敌人”的长数组、以及涉及带权并查集的结构。
总之,还是得多刷题,多积累经验。

猜你喜欢

转载自blog.csdn.net/qq_45698148/article/details/108095133
今日推荐