并查集+带权并查集

并查集

概念:

并查集就是将数组中的数经过树状排列后,
如果寻找两个数是否属于同一集合,
直接找是否属于同一根节点的子树就可以。

路径压缩和按秩合并

路径压缩:

在每次执行 \(get\) 操作的同时,
把访问过的每个节点(也就是所查的元素的祖先,都直接指向树根)

每次 \(get\) 的均摊复杂度为 \(O(logn)\)

按秩合并:

意思就是,在合并时使得树的深度较小,也就是合并到当前深度较小的树上。

这样可以降低查询的时间复杂度,不过一般只写路径压缩即可。

初始代码:

  1. 并查集的存储
    使用一个数组 \(fa\) 保存 \(i\) 父节点: \(fa[i]\)

  2. 并查集的初始化:
    设有 \(n\) 个元素,一开始每个元素各自构成一个集合,因此为:

    for(int i=1;i<=n;i++) fa[i]=i;
    
  3. 并查集的 \(get\) 操作:
    \(x\) 是树根,则 \(x\) 就是集合代表,否则递归访问 \(fa[x]\) 直到根节点:

      int get(int x){
    	if(x==fa[x]) return x;//找到根节点
        return fa[x]=get(fa[x]);//递归+路径压缩,直接将fa[x]指向根节点
    }
    
  4. 并查集的 \(merge\) 操作(无按秩合并):
    合并元素 \(x\) 和元素 \(y\) 的集合,等价于让 \(x\) 的树根作为 \(y\) 的树根的子节点:

       void merge(int x,int y){
            fa[get(x)]=get(y);             
       }               
    

例题:

Acwing 238. 银河英雄传说

分析:

由于这道题需要计算路径,因此可以用数组 \(d[x]\) 来维护前面飞船的长度。

$d[x] $即为 \(x\) 数组距离根节点的长度是多少,可以用 \(sizes\) 数组来赋值 \(d[x]\)

同时,也需要 \(sizes\) 数组来计算一列中总体飞船数量。

最后,如果询问,我们直接算出两飞船 \(d\) 的差即可。

#include<bits/stdc++.h>
using namespace std;
const int N=31010;
int fa[N],n,t,i,j,d[N],sizes[N];//size为记录一列上有几个飞船 
int get(int x){
	if(x==fa[x])//查询到了根节点,则返回根节点
		return x;
	int root=get(fa[x]);//递归计算集合代表 root为树根 
	d[x]+=d[fa[x]];//维护d数组,对边权求和 
	return fa[x]=root;//直接指向树根 
}
void merge(int x,int y){
	x=get(x),y=get(y);
	fa[x]=y,d[x]=sizes[y];//排在x前面是a,d[x]的大小是前面有多少个飞船
	sizes[y]+=sizes[x];//合并,算总体飞船数量 
}
int main()
{
	scanf("%d\n",&t);
	for(i=1;i<=30000;i++) fa[i]=i,sizes[i]=1;//fa表示排在x号战舰前面的那个战舰的编号
	while(t--){
        char ch=getchar();
        scanf("%d %d\n",&i,&j);
        if (ch=='M') merge(i,j);
        else{
            if (get(i)==get(j)) cout<<abs(d[i]-d[j])-1;
            else cout<<"-1";
            puts("");
        }
    }
    return 0;
}

带权并查集

一般的并查集无法存储信息,因此需要带权并查集。

与普通并查集区别:

路径压缩

需要在 \(get(fa[x])\) 前面加一步赋值操作

将查找过程中每个父节点都设为最终得到的那个点

基于路径压缩,带权并查集就是

长成这个样子

因此,在路径压缩过程中,权值也应当做相应的更新

  1. 在路径压缩之前,每个节点都是与其父节点连接着,那个值也是与其父节点之间的权值

  2. 在两个并查集做合并的时候,权值也要做相应的更新

代码:

int find(int x){
	if (x!=fa[x]){
		int y=fa[x];
		fa[x]=find(fa[x]);
		val[x]+=val[y];
	}
	return fa[x];
}

其中,\(val\) 就是子节点的边权值加上父节点的边权值的和。

先记录下原本父节点的编号,压缩后父节点变成根节点,此时父节点的值已经是父节点到根节点的权值了,因此就可以得到当前节点到根节点的权值。

合并

我们要将带有 \(x\) 的树挪到 \(y\) 上,因此先要找到他们的根节点 \(px\)\(py\)

现在是要求 \(px\)\(py\) 之间的权值是多少。

我们可以从这张图中看出来,已知 \(x\)\(px\)\(x\)\(y\) 为读入的 \(s\)\(y\)\(py\)

\(x\)\(py\) 的两条路径长度相同

因此路径长度为 \((-val[x]+s+val[y])=px->py\)

代码:

int px = find(x),py = find(y);
if (px != py){
	fa[px]=py;
	val[px]=+val[y]+s-val[x];
}

3.例题理解

P2294 [HNOI2005]狡猾的商人

此道题差不多就是模板题,只要在判断二者在同一根节点时,其已经存在的权值是否与输入的权值相等就行

因此代码如下:

#include<bits/stdc++.h>
using namespace std;
int dis[105],fa[105],n,m,w;
int find(int x){
	if(x!=fa[x]){
		int root=fa[x];
		fa[x]=find(fa[x]);
		dis[x]+=dis[root];
	}
	return fa[x];
}
void merge(int x,int y,int w)//添加权值操作
{
	int px=find(x),py=find(y);
	if(px!=py){
		fa[px]=py;
		dis[px]=-dis[x]+dis[y]+w;
	}
}
int main()
{
	cin>>w;
	while(w--){
		cin>>n>>m;
		for(int i=0;i<=n;i++) fa[i]=i,dis[i]=0;
		bool flag=true;
		while(m--){
			int x,y,w;
			cin>>x>>y>>w;
			if(flag==false) continue;
			x--;
			if(find(x)!=find(y))//不是同一根节点 
				merge(x,y,w);
			else
				if((dis[x]-dis[y])!=w)
					flag=false;
		}
		if(flag) cout<<"true"<<endl;
		else cout<<"false"<<endl;
	}
	
	return 0;
}
posted @ 2021-10-10 11:30  Evitagen  阅读(217)  评论(0编辑  收藏  举报