并查集 学习笔记
关于并查集这个神奇的东西,之前也有学习过基本的理论和实现,像最小生成树什么的也打过不少,但总感觉自己只会简单的幼稚的基础东西,稍微扩展一点就炸。这几天我也好好地学习了一下并查集的一些奇技淫巧。
没学过并查集的孩子看这里 __戳我__
之前我会的板子,就是很显然的维护集合的并与查。板子就是一下子的事:{
//这是查 inline int find(int x){ return x==fa[x]?x:fa[x]=find(fa[x]); } //这是并 inline void mix(int a,int b){ int f1=find(a),f2=find(b); if(size[f1]>size[f2])swap(f1,f2); fa[f1]=f2;size[f2]+=size[f1]; }
//初始的时候
for(int i=1;i<=n;++i)fa[i]=i; //查询的时候带上路径压缩是最大的优(song)化。按秩合并不值一提,不是特殊情况没什么必要写。
上面就是一些很经典但是很简单的板子。它已经能解决大部分问题。
下面就是一些并查集的扩展了。
1.思路扩展。
举个栗子:noip2010关押罪犯
这个题目困扰了我很久,当初还把它当做2-set问题想过,但实际上这就是一道NOIP题目。
而这种题目的特点就是:代码短,算法简单,思维难度较高(除了NOIP2016,吃×去吧)。
其实说白了还真不复杂,排完序就是一个并查集的事情。
Q:并查集不是只能维护"在一个集合"的信息吗?怎么维护"不在一个集合"的信息呢?
A:是不能维护,但题目是有隐含条件的。"只有两个监狱",代表只有两个集合。一个人在A,那么他的敌人肯定在B,反之亦然。
Q:第一组可以随便放我理解,但是如果出现了一组从未出现过的矛盾,我们又怎么处理呢?
A:既然它是第一次出现,那么它之前的矛盾和它暂时毫无关联,我们只要把他们当成普通的维护,放在不同的集合就好了。
Q:讲这么多,感觉不同并查集还是不可做啊,到底是什么一种方法资磁呢?
A:这就不得不创新一下思维了。我们可以把"x和y不在一个集合"巧妙转化一下,转化成"x在y的敌人的集合,y在x的敌人的集合"。
这样在查询的时候,如果你发现两个人已经在一个集合,就肯定不合法,这就是答案了。
在维护的时候呢,就按照上面那句话说的做就好啦!
具体实现下,敌人集合可以通过(x+n)代表,只要将并查集数组开两倍就好啦。
如果你开局就给每个人设置了一个假想敌ri,这个假想敌只和i有矛盾,显然不会影响答案。
这个时候再处理矛盾就很形象很好理解了。
#include <iostream> #include <cstdio> #include <cstdlib> #include <algorithm> #include <vector> #include <cstring> #include <queue> #define LL long long int #define ls (x << 1) #define rs (x << 1 | 1) using namespace std; const int N = 200010; struct Data{ int x,y,w; bool operator < (const Data &b)const{ return w>b.w; } }rem[N]; int n,m,fa[N],Ans; int gi() { int x=0,res=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')res*=-1;ch=getchar();} while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar(); return x*res; } inline int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);} int main() { n=gi();m=gi(); for(int i=1;i<N;++i)fa[i]=i; for(int i=1;i<=m;++i){ int x=gi(),y=gi(),z=gi(); rem[i]=(Data){x,y,z}; } sort(rem+1,rem+m+1); for(int i=1;i<=m;++i){ int x=rem[i].x,y=rem[i].y; int f1=find(x),ff1=find(x+n); int f2=find(y),ff2=find(y+n); if(f1^f2) fa[f1]=ff2,fa[f2]=ff1; else Ans=rem[i].w,i=m; } printf("%d\n",Ans); return 0; }
那么我们再看一下 NOI2001食物链 ,是不是完全一样的题目?
只需要充分挖掘题目的信息:{
第一种智障假话不提。
// bool operator = {int x,int y}const{return x和y在同一个集合;}
1.D=1,x,y{
如果(x=y吃 || x=y被吃 || x吃=y || x吃=y被吃 || x被吃=y || x被吃=y吃)假话;
否则真话{并:x与y,x吃与y吃,x被吃与y被吃;}
}
2.D=2,x,y{
如果(x=y || x=y吃 || x吃=y吃 || x吃=y被吃 || x被吃=y || x被吃=y被吃)假话;
否则真话{并:x与y被吃,x吃与y,x被吃与y吃;}
}
}
可以看见具有条件整齐性和对齐性(雾)。
总结:看来NOIP很喜欢出前十年左右的NOI题目弱化版。
#include <iostream> #include <cstdio> #include <cstdlib> #include <algorithm> #include <vector> #include <cstring> #include <queue> #define LL long long int #define ls (x << 1) #define rs (x << 1 | 1) using namespace std; const int N = 50010; int n,m,fa[N*4],Ans; int gi() { int x=0,res=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')res*=-1;ch=getchar();} while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar(); return x*res; } inline int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);} int main() { n=gi();m=gi(); for(int i=0;i<N*3;++i)fa[i]=i; while(m--){ int kind=gi(),x=gi(),y=gi(); if(x>n || y>n){Ans++;continue;} if(kind==1){ int f1=find(x),feat1=find(x+n),feated1=find(x+n+n); int f2=find(y),feat2=find(y+n),feated2=find(y+n+n); if(f1==feat2 || f1==feated2 || feat1==feated2 || f2==feat1 || f2==feated1 || feat2==feated1) {Ans++;continue;} else fa[f2]=f1,fa[feat2]=feat1;fa[feated2]=feated1; } else{ if(x==y){Ans++;continue;} int f1=find(x),feat1=find(x+n),feated1=find(x+n+n); int f2=find(y),feat2=find(y+n),feated2=find(y+n+n); if(f1==f2 || f1==feat2 || feat1==feated2 || feat1==feat2 || feated1==f2 || feated1==feated2) {Ans++;continue;} else fa[f2]=feat1,fa[feat2]=feated1,fa[feated2]=f1; } } printf("%d\n",Ans); return 0; }
2.内容扩展
常见的并查集只维护了一个上级数组,最多再加一个秩。但有些丧心病狂的出题人不满足如此,要你在上面写出一朵花。
比如说: NOI2002 银河英雄传说
很明显是并查集是吧,但是好像还要求一个深度?
于是就变成了带边权的并查集。
带权并查集:维护当前点到fa的距离d[x]。
事实上,到根的距离dis(x)=d[x]+dis(fa[x])。
路径压缩后,dis[fa[x]]变成了d[fa[x]]。
d[x]变成了d'[x]=dis(x)=d[x]+d[fa[x]]。
所以在改fa[x]之前d[x]+=d[fa[x]]就好了。
经过仔细思考后,定义dis为到根的距离,size为一溜船的大小(秩)。
关键就在于边权的维护?
考虑到之前的dis是到自己指向的点的距离,find之后的dis[fa]就是fa到根的距离。
所以就是:dis[x]+=dis[fa];
剩下的就很简单了。
#include <algorithm> #include <iostream> #include <cstdlib> #include <cstring> #include <cstdio> #include <cmath> using namespace std; const int N = 30010; int fa[N],dis[N],size[N],m; inline int ABS(int x){return (x^(x>>31))-(x>>31);} inline int gi() { int x=0,res=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')res=-res;ch=getchar();} while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar(); return x*res; } inline int gc() { char ch=getchar(); while(ch<'A'||ch>'Z')ch=getchar(); return ch=='C'?1:2; } inline int find(int x) { if(fa[x]==x)return x; int nfa=fa[x];fa[x]=find(fa[x]); dis[x]+=dis[nfa]; return fa[x]; } inline void work1(int u,int v) { int f1=find(u),f2=find(v); if(f1!=f2)printf("-1\n"); else printf("%d\n",ABS(dis[u]-dis[v])-1); } inline void work2(int u,int v) { int f1=find(u),f2=find(v); fa[f1]=f2;dis[f1]=size[f2];size[f2]+=size[f1]; } int main() { for(int i=1;i<=N;++i) fa[i]=i,size[i]=0,size[i]=1; m=gi(); while(m--) { int type=gc(),u=gi(),v=gi(); if(type==1)work1(u,v); else work2(u,v); } return 0; }
还记得有一个貌似是可撤销的并查集?哎呀我找不到是哪一题了。
主要思路就是不加路径压缩,所以要加按秩合并。
然后把每一次的修改加到一个栈里面就好了。
退栈的时候就改回来size和fa就好了。