zoukankan      html  css  js  c++  java
  • 并查集 学习笔记

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

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

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

    //这是查
    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
    ",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
    ",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
    ");
        else printf("%d
    ",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就好了。

  • 相关阅读:
    Centos7开机启动脚本代码
    浏览器标签上的 favicon 图标是怎么实现的?
    Android添加权限大讲解
    一个安卓小项目(3)——安卓FTP方式发送文件到服务器
    一个安卓小项目(2)——各模块具体流程
    XML中特殊符号转义实体
    Android Activity生命周期
    牛是怎么死的原文+感想
    一个安卓小项目(1)——需求与分工
    不忘初心
  • 原文地址:https://www.cnblogs.com/fenghaoran/p/6898247.html
Copyright © 2011-2022 走看看