$ $学习总结:并查集
蒟蒻的第一篇博客,如有bug,请大佬提出,勿喷。
并查集:
并查集虽说是集合,不过个人觉得类似树形结构,像森林,刚开始每一个节点是一个森林,不断把森林合并,形成树。
• 是一组不相交的集合。即集合之间没有公共元素,它们的交集是空集。
• 因此没有“求集合的交集”和“求集合的差集” 操作。
• 至于“求集合的并集”运算,则只需简单地将两个集合的元素合并在一起就行了,不用考虑剔除重复元素的问题。
上模板:
【 P3367 【模板】并查集】
题目描述
如题,现在有一个并查集,你需要完成合并和查询操作。
输入输出格式
输入格式:
第一行包含两个整数N、M,表示共有N个元素和M个操作。
接下来M行,每行包含三个整数Zi、Xi、Yi
当Zi=1时,将Xi与Yi所在的集合合并
当Zi=2时,输出Xi与Yi是否在同一集合内,是的话输出Y;否则话输出N
输出格式:
如上,对于每一个Zi=2的操作,都有一行输出,每行包含一个大写字母,为Y或者N
代码:
#include
using namespace std;
int father[100000+10];
void init(){ //初始化
for(int i=0;i<10000+10;i++) father[i]=i; //爸爸就是自己
}
int find(int x){ //找祖先
if(father[x]==x) return x; //如果爸爸是自己的话,那么他就是这整个集合的root,及祖先。
else return father[x]=find(father[x]); //否则继续向上找
}
void findans(int x,int y){ //判两数是否是否在同一集合(并查集)
int i=find(x); //找祖先
int j=find(y); //同上
if(i==j) cout<<'Y'<>n>>m;
init();
for(int i=1;i<=m;i++){
int e,a,b;
cin>>e>>a>>b;
if(e==1) //操作:合并
union_(a,b);
else //操作:询问
findans(a,b);
}
return 0;
}
带秩并查集:
什么是秩:秩就是并查集的节点数。
经典题:【打击犯罪】
某个地区有n(n≤1000)个犯罪团伙,当地警方按照他们的危险程度由高到低给他们编号为1-n,他们有些团伙之间有直接联系,但是任意两个团伙都可以通过直接或间接的方式联系,这样这里就形成了一个庞大的犯罪集团,犯罪集团的危险程度由集团内的犯罪团伙数量唯一确定,而与单个犯罪团伙的危险程度无关(该犯罪集团的危险程度为n)。现在当地警方希望花尽量少的时间(即打击掉尽量少的团伙),使得庞大的犯罪集团分离成若干个较小的集团,并且他们中最大的一个的危险程度不超过n/2。为达到最好的效果,他们将按顺序打击掉编号1到k的犯罪团伙,请编程求出k的最小值。
【输入】
第一行一个正整数n。接下来的n行每行有若干个正整数,第一个整数表示该行除第一个外还有多少个整数,若第i行存在正整数k,表示i,k两个团伙可以直接联系。
【输出】
一个正整数,为k的最小值。
【输入样例】
7
2 2 5
3 1 3 4
2 2 4
2 2 3
3 1 6 7
2 5 7
2 5 6
【输出样例】
1
【思路】:审题,题目要求,按顺序打击1~k的罪犯团伙,也就是说,要打k号犯罪团伙,必须先打掉1~k-1号犯罪团伙。所以,我们可以从后往前建并查集,一旦发现并查集的秩超过了题目要求(n/2)那么直接输出结果(当前操作的编号)。
【代码】
#include
#include
#include
#include
using namespace std;
int father[100000+10],num[100000+10],dis[100000+10],ans,k,o,e[1000001],a[1001][1001];
void init() //初始化
{
for(int i=0;i<50000+10;i++){father[i]=i;num[i]=1;}
}
int find(int x) //找祖先
{
if(x!=father[x])
{
father[x]=find(father[x]);
}
return father[x];
}
void merge(int x,int y)
{
int a1=find(x),a2=find(y);
if(a1!=a2)
{
father[a1]=a2;
num[a2]+=num[a1]; //秩累加
if(num[a2]>k){cout<=k){cout<>m;
k=m/2;
init();
for(int i=1;i<=m;i++)
{
cin>>e[i];
for(int j=1;j<=e[i];j++)
{
cin>>a[i][j];
}
}
for(int i=m;i>=1;i--)
{
o=i;
for(int j=1;j<=e[i];j++)
{
if(ia[i][j]) a[i][j]已经被打掉了,所以没用
merge(i,a[i][j]); //建立并查集
}
}
return 0;
}
带权并查集&&n倍空间
什么是权:权就是权值……
经典题:【食物链】&&【银河英雄传说】
P1196 [NOI2002]银河英雄传说
题目描述
公元五八○一年,地球居民迁移至金牛座α第二行星,在那里发表银河联邦创立宣言,同年改元为宇宙历元年,并开始向银河系深处拓展。
宇宙历七九九年,银河系的两大军事集团在巴米利恩星域爆发战争。泰山压顶集团派宇宙舰队司令莱因哈特率领十万余艘战舰出征,气吞山河集团点名将杨威利组织麾下三万艘战舰迎敌。
杨威利擅长排兵布阵,巧妙运用各种战术屡次以少胜多,难免恣生骄气。在这次决战中,他将巴米利恩星域战场划分成30000列,每列依次编号为1, 2, …, 30000。之后,他把自己的战舰也依次编号为1, 2, …, 30000,让第i号战舰处于第i列(i = 1, 2, …, 30000),形成“一字长蛇阵”,诱敌深入。这是初始阵形。当进犯之敌到达时,杨威利会多次发布合并指令,将大部分战舰集中在某几列上,实施密集攻击。合并指令为M i j,含义为让第i号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第j号战舰所在的战舰队列的尾部。显然战舰队列是由处于同一列的一个或多个战舰组成的。合并指令的执行结果会使队列增大。
然而,老谋深算的莱因哈特早已在战略上取得了主动。在交战中,他可以通过庞大的情报网络随时监听杨威利的舰队调动指令。
在杨威利发布指令调动舰队的同时,莱因哈特为了及时了解当前杨威利的战舰分布情况,也会发出一些询问指令:C i j。该指令意思是,询问电脑,杨威利的第i号战舰与第j号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。
作为一个资深的高级程序设计员,你被要求编写程序分析杨威利的指令,以及回答莱因哈特的询问。
最终的决战已经展开,银河的历史又翻过了一页……
【样例输入】
4
M 2 3
C 1 2
M 2 4
C 4 2
【样例输出】
-1
1
说明
【样例说明】
战舰位置图:表格中阿拉伯数字表示战舰编号
【思路】
在建并查集的过程中储存两战舰的距离,找答案的时候加以处理。
【代码】
#include
#include
#include
using namespace std;
int father[100000+10],num[100000+10],dis[100000+10],ans;
void init() //初始化
{
for(int i=0;i<50000+10;i++){father[i]=i;num[i]=1;}
}
int find(int x) //找祖先
{
if(x!=father[x])
{
int k=father[x];
father[x]=find(father[x]);
dis[x]+=dis[k]; //累加两战舰的距离
num[x]=num[father[x]]; //加权
}
return father[x];
}
int findans(int a,int b) //找答案
{
int r1=find(a),r2=find(b);
if(r1!=r2)
{
return -1;
}
else{
return abs(dis[a]-dis[b])-1;
}
}
void merge(int x,int y) //模板
{
int a1=find(x),a2=find(y);
if(a1!=a2)
{
father[a1]=a2;
dis[a1]=dis[a2]+num[a2];
num[a2]+=num[a1]; //权
num[a1]=num[a2];
}
}
int main(){
int n,m,p;
cin>>m;
init();
for(int i=1;i<=m;i++)
{
int a,b;
char e;
cin>>e>>a>>b;
if(e=='M')
merge(a,b); //建并查集
else
{
ans=findans(a,b);
printf("%d\n",ans);
}
}
return 0;
}
n倍空间并查集
【团伙】此题是双倍空间,【食物链】是三倍空间,当然【食物链】也可以用带权并查集来做。
【食物链】
描述
动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是"1 X Y",表示X和Y是同类。
第二种说法是"2 X Y",表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。
输入
第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。
输出
只有一个整数,表示假话的数目。
样例输入
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
样例输出
3
【思路】
开三倍空间,指的是把数组开三倍空间,分别存同类,猎物,天敌。然后把每一次输入的数据进行判断,如果发现不符题意,那么就是谎话,否则把所有有关联的同类连接在一起。
【代码】
#include
#include
#include
#include
using namespace std;
long long father[150000+10],t[1000001],ans,o,x,y,k,vis[100001],a,b,c;
void init(int n){ //三倍空间初始化
for(int i=1;i<=n;i++)
{
father[i]=i;
father[i+n]=i+n;
father[i+n+n]=i+n+n;
}
}
int find(int x) //找祖先
{
if(father[x]==x)
{
return x;
}
else
{return father[x]=find(father[x]);}
}
void union_(int x,int y){ //模板
int i=find(x);
int j=find(y);
if(i!=j){father[i]=j;}
}
int main()
{
int n,m,p;
cin>>n>>m;
init(n);
for(int i=1;i<=m;i++)
{
cin>>c>>a>>b;
if(a>n||b>n) //判断符不符题意
{
ans++;
continue;
}
if(c==1) //同类情况
{
if(find(a+n)==find(b)||find(a+n+n)==find(b)) //判断符不符题意
ans++;
else
{
union_(a,b); //同类处理
union_(a+n,b+n);
union_(a+n+n,b+n+n);
}
}
else
{
if(a==b) //判断符不符题意
{
ans++;
continue;
}
if(find(a)==find(b)||find(a+n+n)==find(b))
ans++;
else
{
union_(a,b+n+n); //非同类处理
union_(b,a+n);
union_(a+n+n,b+n);
}
}
}
cout<
总结:
1、并查集要注意关系,不能漏建,不能重建,不然会WA。
2、Find(),merge()的模板要记住,一般进行修改都会在这两个函数里改。
3、合并的时候有时要注意方向,不要搞反了。
随便给点题:
UVA1316 Supermarket
UVA615 Is It A Tree?
UVA1197 The Suspects
P1195 口袋的天空
P1525 关押罪犯
POJ 1988 Parity game
(题解链接:大佬博客)