并查集 学习笔记

  关于并查集这个神奇的东西,之前也有学习过基本的理论和实现,像最小生成树什么的也打过不少,但总感觉自己只会简单的幼稚的基础东西,稍微扩展一点就炸。这几天我也好好地学习了一下并查集的一些奇技淫巧。

  没学过并查集的孩子看这里 __戳我__

  之前我会的板子,就是很显然的维护集合的并与查。板子就是一下子的事:{

//这是查
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就好了。

posted @ 2017-05-25 13:13  Fenghr  阅读(540)  评论(1编辑  收藏  举报