专题 并查集

并查集

作用:

1.将两个集合合并
2.询问两个元素是否在一个集合当中
时间复杂度近乎O(1)

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

Q:

1.如何判断树根: p[x]=x
2.如何求x的集合编号 while(p[x]!=x) x=p[x]
3.如何合并两个集合 : px是x的集合编号,py是y的集合编号。p[x]=y

优化:路径压缩

在这里插入图片描述

合并集合

注意findif(p[x)!=x)这里是if不是while

#include<iostream>

using namespace std;

const int N=1e6;

int p[N];

int find(int x){//查找x的根节点(集合编号) 
	if(p[x]!=x)//如果不是根节点
		p[x]=find(p[x]);//查找的同时也进行了路径压缩优化
	return p[x];
}

void merge(int a,int b){//合并操作 
	p[find(b)]=find(a);//注意这里两个都是find() 
}

string s;
int a,b;
int n,m;

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		p[i]=i;//因为刚开始每个元素分别属于一个集合,因此需要初始化 
	while(m--){
		cin>>s>>a>>b;
		if(s=="M"){
			merge(a,b);
		}
		
		if(s=="Q"){
			if(find(a)==find(b))
				cout<<"Yes"<<endl;
			else
				cout<<"No"<<endl;
		}	
	}
	return 0;
} 

查找某个集合中元素个数

注意先更新个数再merge
如果先merge,那么更新的元素个数就是合并之后

#include<iostream>

using namespace std;

const int N=1e6;

int p[N];

int find(int x){//查找x的根节点(集合编号) 
	if(p[x]!=x)//如果不是根节点
		p[x]=find(p[x]);//查找的同时也进行了路径压缩优化
	return p[x];
}

void merge(int a,int b){//合并操作 
	p[find(b)]=find(a);//注意这里两个都是find() 
}

string s;
int a,b;
int n,m;
int si[N];


int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		p[i]=i;//因为刚开始每个元素分别属于一个集合,因此需要初始化
		si[i]=1; 
	}
		 
	while(m--){
		cin>>s;
		if(s=="C"){
			cin>>a>>b;
			if(find(a)!=find(b)){
				si[find(a)]+=si[find(b)];//注意这里要先更新总数再做合并操作 
				merge(a,b);
			}
		}
		
		if(s=="Q1"){
			cin>>a>>b;
			if(find(a)==find(b))
				cout<<"Yes"<<endl;
			else
				cout<<"No"<<endl;
		}	
		if(s=="Q2"){
			cin>>a;
			cout<<si[find(a)]<<endl;
		}
	}
	return 0;
} 

食物链
食物链

真的好难
在这里插入图片描述

#include<iostream>

using namespace std;

int n,k;
const int N=5*1e5;
const int M=3*1e6;
int p[N];
int d[N];

int find(int x){
	if(p[x]!=x)	{
		/*
		int t=p[x];//先存一下该点,因为下一步被更新 
		p[x]=find(p[x]);//路径压缩(表明该点已知) 
		d[x]+=d[t];
		*/
		int t = find(p[x]);
		//利用find()得到x所在的根节点是t
		//但是此时p[x]并没有更新,不过在执行find的过程中,从x到跟的路径上的所有点都更新了p[]和d[] 
        //因此下面语句中的d[p[x]]是p[x]到根节点的距离,就是x的父节点到根节点的距离
		//如果x的父节点就是根节点,即p[x]=x,那么d[x]+=0,d[x]仍是原值,表示其到父节点的距离,也是根节点距离 
		d[x] += d[p[x]];
        //d[i]的含义是x到其父节点的距离,并不一定是根节点,因为不一定更新到根节点了(或者根节点又加入了另一个树中,就不是根节点了) 
		//那么d[i]到 
        p[x] = t;//更新x的根节点,路径上的那些都已经更新过了 
	}
	return p[x];
}



int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		p[i]=i;
		d[i]=0;//其实也可以省略,全局数组本来就是0 
	}
	
	int q,x,y;
	int ans=0;
	
	while(k--){
		cin>>q>>x>>y;
		
		int px=find(x);int py=find(y);
		//找到各自根节点 
		//而且因为使用了find(),所以当前的d[x],d[y]都是到根节点的距离
		//而p[x]p[y]都是根节点的值 
		
		if(x>n||y>n)	ans++;
		
		
		else{
			if(q==1){
				//表明x,y是同类
				if(px==py&&(d[x]-d[y])%3!=0)	ans++;//如果已经在一个集合中但已经知道不是一个类,因此这个是假话
				//注意第二个判断不写d[x] % 3 != d[y] % 3,因为d[i]可能是负数 
				
				else if(px!=py){
					//说明是真话,那么就维护
					p[px]=py;//将x,y维护到同一个集合中 
					d[px]=d[y]-d[x]; 
					//这个看截图 
				}		 
				
			}
			else{
				if(x==y)	ans++;
				
				else if(px==py){
					//根据我们的定义%=1吃%=0,%=2吃%=1,%=0吃%=2 
					//x吃y 
					//0-2-1模3也可以判断 
					if((d[x]-d[y]-1)%3){
						//如果不是满足的情况
						ans++; 
					}
				}
				else if(px!=py){
					//x吃y
					p[px]=py;
					d[px]=d[y]-d[x]+1;
				}
			}
			
		}
		
	}
	
	cout<<ans;
	return 0;
}

字符串归类
字符串归类

写了一个超时的算法,过了11/20

/*
给定 n 个由小写字母构成的字符串。

现在,请你对它们进行归类。

对于两个字符串 a 和 b:

如果至少存在一个字母在 a 和 b 中同时出现,则 a 和 b 属于同一类字符串。
如果字符串 c 既与字符串 a 同类,又与字符串 b 同类,则 a 和 b 属于同一类字符串。
请问,最终所有字符串被划分为多少类。

输入格式
第一行包含整数 n。

接下来 n 行,每行包含一个仅由小写字母构成的字符串。

注意,输入字符串可能相同。

输出格式
一个整数,表示最终所有字符串被划分为的类的数量。

数据范围
前 6 个测试点满足 1≤n≤10。
所有测试点满足 1≤n≤2×105,输入字符串的长度范围 [1,50],所有输入字符串的总长度范围 [1,106],所有字符串均由小写英文字母构成。

输入样例1:
4
a
b
ab
d
输出样例1:
2
输入样例2:
3
ab
bc
abc
输出样例2:
1
输入样例3:
1
abcdefghijklmn
输出样例3:
1
*/

//一般一秒对应4*10^8次计算 

#include<iostream>
#include<set>

using namespace std;

const int N=3*1e5;

int judge(string x,string y){
	set <char> a;
	set <char> b;
	set <char> c;
	for(auto m:x)
		a.insert(m);
	for(auto m:y)
		b.insert(m);
	for(auto m:x)
		c.insert(m);
	for(auto m:y)
		c.insert(m);
	if(c.size()<a.size()+b.size())
		return 1;
	return 0;
}
int p[N]; 
int find(int x){
	if(p[x]!=x)	p[x]=find(p[x]);
	return p[x];
}


string a[N];
int n;


int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		p[i]=i;
	}
		
		
		
	for(int i=1;i<n;i++){
		for(int j=i+1;j<=n;j++){
			if(judge(a[i],a[j])==1){
				//如果某两个字符串同类,查找是否其中已经有归类的
				int pi=find(i),pj=find(j);
				if(pi==pj)
					continue;
				else if(pi!=i||pj!=j){
					p[pi]=pj;
				}	
				else
					p[i]=find(j);
			}
		}
	}	
	
	int ans=0;
	for(int i=1;i<=n;i++)
		if(p[i]==i)
			ans++;
	cout<<ans;
	return 0;
}

看了题解后发现,并不需要对于每个字符串处理。
其实既然具有相同字母的字符串是一类,并且和同一个字符串是一类的两个字符串是一类。那么实际上最多就26类(字符串分别是26个互不相同的字母时),因此可以对字符串的元素进行集合合并
合并规则
所有含有字母a的为一个集合,含有字母b的为一个集合,以此类推。这就满足了如果至少存在一个字母在 a 和 b 中同时出现,则 a 和 b 属于同一类字符串这一条。
然后对于如果字符串 c 既与字符串 a 同类,又与字符串 b 同类,则 a 和 b 属于同一类字符串这一条,翻译过来就是,如果某个字符串既含有a,又含有b,那么ab为一类。
值得一提的是,对于某个字符串,并不需要执行时间复杂度为O(n方)的两两判别,只需要选定第一个字符,然后直接O(n)就可以,因为一个字符串中所有的字符都会维护到同一个集合中。

#include<iostream>

using namespace std;

const int N=27;

int p[N];
int j[N];//判断元素是否用过 
int n;
string s;

int find(int x){
	if(p[x]!=x) p[x]=find(p[x]);
	return p[x];
}


int main(){
	cin>>n;
	for(int i=1;i<=26;i++)
		p[i]=i;
	while(n--){
		cin>>s;
		//记录第一个 
		j[s[0]-'a'+1]=1;//表明这个元素出现过
		p[s[0]-'a'+1]=find(s[0]-'a'+1);
		int x=find(s[0]-'a'+1);//第一个字符所在集合为x 
		
		//如果某个字符串第一个元素是在已有的集合中,那么后面所有元素都归入这个集合了 
		for(int i=1;i<s.size();i++){
			int m=s[i]-'a'+1;//字母编号 
			j[m]=1;
			int pm=find(m);//找到其根 
			if(pm!=x){//如果当前两个集合未合并 
				p[pm]=x;
			}
		}
	}
	
	int ans=0;
	for(int i=1;i<=26;i++){
		if(p[i]==i&&j[i])
			ans++; 
	}
	cout<<ans;
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
并查集是一种常用的数据结构,用于管理一个不相交集合的数据。在并查集中,每个元素都有一个父节点指向它所属的集合的代表元素。通过查找和合并操作,可以判断两个元素是否属于同一个集合,并将它们合并到同一个集合中。 在解决某些问题时,可以使用并查集进行优化。例如,在区间查询中,可以通过优化并查集的查询过程,快速找到第一个符合条件的点。 对于拆边(destroy)操作,一般的并查集无法直接实现这个功能。但是可以通过一个巧妙的方法来解决。首先,记录下所有不会被拆除的边,然后按照逆序处理这些指令。遇到拆边操作时,将该边重新加入并查集中即可。 在实现并查集时,虽然它是一种树形结构,但只需要使用数组就可以实现。可以通过将每个元素的父节点记录在数组中,来表示元素之间的关系。通过路径压缩和按秩合并等优化策略,可以提高并查集的效率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [「kuangbin带你飞」专题并查集专题题解](https://blog.csdn.net/weixin_51216553/article/details/121643742)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [并查集(详细解释+完整C语言代码)](https://blog.csdn.net/weixin_54186646/article/details/124477838)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值