并查集的两类扩展

介绍

本篇博客是我的Luogu Blog上两篇博文的总和,但优化了阅读体验,主要介绍种类并查集和带权并查集

种类并查集原文

带权并查集原文

1.带权并查集部分

我们在做并查集的一些题的时候,有时候会遇到这样的问题

  • \(X\)\(Y\) 之间差了几代,如果不在一个家族里就输出 \(-1\)

数据小的话,模拟水水还能过,但是我们既然学了并查集这种神奇的算法,就要用正解去 \(\color{#228B22}{AC}\)

来看例题

P1196 银河英雄传说

题意和我上面叙述的差不多,我们来考虑解题方式

判断是否在同一列以及如何一次操作合并两个队列的问题我们用普通的并查集就能实现,难点是求两个点之间的距离

在上数学课的时候,我们会学一种特殊的距离求法,存两个点到另一个定点的距离,相减来求距离,我们就用这种方法来实现在并查集中求距离

我们先设置几个数组

f[i]表示i的祖先

front[i]表示i到祖宗的距离(即隔了几代)

num[i]表示以i为队头家族的长度

在合并俩个家族的时候,我们只需要对队头(祖宗)进行操作即可

我们把祖宗的 front 加上要合并的家族的任意节点的 num ,原来祖宗的 num 归零,现在祖宗的 num +原来祖宗的 num ,完成合并的操作

		f[fa]=fb;
		front[fa]+=num[fb];
		num[fb]+=num[fa];
		num[fa]=0;

但为了之上的所有操作,我们需要先找到你的祖宗......

inline int find(int x)
{
	if(f[x]==x) return f[x];
	int fx=find(f[x]);
	front[x]=front[x]+front[front[fx]];//仔细想想这行有啥用
	return fx;
}

上面的那一行加上去的代码实际上是维护了 front 数组,让它始终是正确的值(即更新的i到队头的距离)

完整Code

#include<cstdio>
#include<iostream>
#include<cmath>
using namespace std;
int t;
char inm;
int a,b;
int f[30005];//i的祖先 
int front[30005];//表示i到目前队头的距离 
int num[30005];//num[i]表示以i为队头的队列长度 
inline int find(int x)
{
	if(f[x]==x) return f[x];
	int fx=find(f[x]);
	front[x]+=front[f[x]];
	return f[x]=fx;
}
int main()
{
	cin>>t;
	for(int i=1; i<=30005;i++)
	{
		f[i]=i;
		front[i]=0;
		num[i]=1;
	}
	while(t--)
	{
		cin>>inm>>a>>b;
		int fa=find(a);
		int fb=find(b);
		if(inm=='M')
		{
			f[fa]=fb;
			front[fa]+=num[fb];
			num[fb]+=num[fa];
			num[fa]=0;
		}
		if(inm=='C')
		{
			if(fa==fb)
			{
				cout<<abs(front[a]-front[b])-1<<endl;
			}
			else
			{
				cout<<-1<<endl;
			}
		}
	}
	return 0;
}

2.种类并查集部分

P2024 [NOI2001] 食物链

乍看此题没有思路(几乎所有人都会这样),事实上比赛的时候拿分的关键不是算法能否写对,而是你能否找到这道题该用什么算法,我们不做过多解释,直接开始思路讲解

A 吃 B,B 吃 C,C 吃 A,我们整理一下,用“天敌”和“同类”“猎物”来表示关系,不难发现他们之间的关系

(同类表示同一种动物,天敌表示谁吃谁,猎物表示谁被谁吃)

  1. 若X为Y的同类,他们所有的天敌和猎物就会在一个集合里

  2. 若X为Y的天敌,Y的天敌和X就会在一个集合里,Y的猎物和X的天敌也是(画个图试试),X的猎物和Y也是

我们开一个\(3\)倍空间的f[3n]数组(为甚是\(3\)倍看完就懂了)

我们把这个数组划分为\(3\)

  • 1.\(1-n\) 的范围存储这个动物的同类

  • 2.\(n+1-2n\) 的范围存储这个动物的天敌

  • 3.\(2n+1-3n\) 的范围存储这个动物的猎物

这样我们就可以方便的进行查询与合并操作了

接下来考虑如何判断输入的话是否为假话

分情况讨论

First 当X与Y是同类时

  • 1.\(x>n\) 或者 \(y>n\)

  • 2.\(x+n\)\(y\) 在同一个集合(天敌关系)

  • 3.\(x+2n\)\(y\) 在同一个集合(猎物关系)

Second 当X与Y是吃和被吃关系时

  • 1.同上

  • 2.\(x\)\(y\) 在同一个集合(同种不可能互相残杀)

  • 3.\(x\)\(y\) 相等(自己吃自己?)

  • 4.\(x+n\)\(y\) 在同一个集合(反了)

解决了这一步,我们考虑合并

在第一种情况时,合并操作很简单,我们只需要把关于他俩的所有关系的集合都合并即可

join(x,y);
join(x+n,y+n);
join(x+2*n,y+2*n);

在第二种情况时

join(x+2*n,y);//x的猎物和y合并(天敌的猎物就是同类) 
join(x,y+n);//x和y的原有天敌合并 
join(x+n,y+2*n);//x的天敌和y的猎物合并 

至此,我们解决了所有问题

#include<cstdio>
#include<iostream>
using namespace std;
int f[150010];
int n,k,ans;
int a,x,y;
inline int find(int xx)
{
	if(f[xx]==xx)
	{
		return f[xx];
	}
	return f[xx]=find(f[xx]);
}
inline void join(int xx,int yy)
{
	int f1=find(xx),f2=find(yy);
	if(f1!=f2) f[f1]=f2;
	return;
}
int main()
{
	cin>>n>>k;
	for(register int i=1; i<=3*n;i++) f[i]=i;
	while(k--)
	{
		scanf("%d%d%d",&a,&x,&y);
		if(a==1)//x和y是同类 
		{
			if(x>n||y>n||find(x+n)==find(y)||find(x+2*n)==find(y)) ans++;//1.x,y大于n 2.y是x天敌 3.y是x猎物 
			else
			{
				join(x,y);
				join(x+n,y+n);
				join(x+2*n,y+2*n);//所有的都是相同的 
			}
		}
		if(a==2)
		{
			if(x==y||x>n||y>n||find(x)==find(y)||find(x+n)==find(y))//1.同上 2.x和y相等(自己吃自己) 3.x和y是同类 4.y是x的天敌 
			{
				ans++;
			}
			else
			{
				join(x+2*n,y);//x的猎物和y合并(天敌的猎物就是同类) 
				join(x,y+n);//x和y的原有天敌合并 
				join(x+n,y+2*n);//x的天敌和y的猎物合并 
			}
		}
	}
	cout<<ans;
}

结束语

在翻这个题的题解的时候,看到了一段让我印象深刻的文字

感觉说的好有道理,当我在学习一些高深的图论和数论的时候,也经常会发出这样的疑问,现在想起来,不过也是这样罢了

posted @ 2021-07-03 20:05  Edolon  阅读(36)  评论(0编辑  收藏  举报
Live2D