关于并查集这个神奇的东西,之前也有学习过基本的理论和实现,像最小生成树什么的也打过不少,但总感觉自己只会简单的幼稚的基础东西,稍微扩展一点就炸。这几天我也好好地学习了一下并查集的一些奇技淫巧。
没学过并查集的孩子看这里 __戳我__
之前我会的板子,就是很显然的维护集合的并与查。板子就是一下子的事:{
//这是查
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就好了。