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就好了。

  • 相关阅读:
    如何在iTerm2中配置oh my zsh?
    sublime中格式化jsx文件
    ES6 new syntax of Literal
    ES6 new syntax of Rest and Spread Operators
    How to preview html file in our browser at sublime text?
    ES6 new syntax of Default Function Parameters
    ES6 new syntax of Arrow Function
    七牛云2018春招笔试题
    Spring-使用注解开发(十二)
    Spring-声明式事物(十一)
  • 原文地址:https://www.cnblogs.com/fenghaoran/p/6898247.html
Copyright © 2011-2022 走看看