并查基类型总结

暑期ACM第二练--并查集
第一类:普通并查集:
    感觉学会模板,套模板,必过,超时的话就加点优化,数据过大就加优化,init()以及最大值处是优化的地方,以及数组的个数(自身经历,开过大的数组会耗时);
第二类:带权并查集:
    http://www.cppblog.com/yuan1028/archive/2011/02/13/139990.html
    看了下这个博客,感觉挺好的。但是感觉有的还是分不太清楚,又去网上做了些题,查了一下。
    
    个人看法:
    带权并查集的题:
        是一些不仅仅是单纯的A,B有关系,在一棵树上。更是A.B节点的值,有些什么关系,数字关系(判断存储值),逻辑关系(判断对错)->找出对应关系(分帮派,昆虫真爱判断)
        总之,这些题在建立一个并查集的基础上都需要有关系数组与之对应。
        而与普通并查集模板相对应的:
            合并(Union/merge)函数是对两棵树的本体进行操作,并且对于作为主体的那颗树所有经历的节点进行操作。例如A树挂到B树上。是对B树和A的根操作。而A的节点并未改变。
            查找 (find)函数:是对一棵树的从给定参数X 进行查找以及逐步合并。也就是对给定的关系进行操作




1. 对应初步的带权并查集的操作:
    带权的并查集是在并查集模板上加之以关系数组进行跟进,大体意思是上面所述。


hdu 2818 
题目描述:一开始有n个积木,形成了每个堆只有一个的分组
输入P,接下来有P个操作,操作有两种:


M a b如果a,b没有在一个堆里,把包含a堆积木放在包含b积木的堆上。


C a 输出有多少个积木被压在了a之下。


分析:father表示x堆的根节点,count[x]表示x堆总个数,under[x]表示在x下有几个积木
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Maxn 300000
int count[Maxn];   //count为每个堆中积木个数
int under[Maxn];   //under为每个堆下面所有的积木个数
int father[Maxn];  //father为原来1 2 3 4 。。。n的积木堆的父节点
int N;
void init ()
{
    int i;
    for(i=0;i<=N;i++)
    {
        count[i]=1;
        under[i]=0;
        father[i]=i;
    }
}
int find(int x)//在结尾find(i)时,更新每个叶节点的权值和父节点的值,而Union调用时也可以执行,然而在Union调用时只是单纯调用根节点,因此仍未对叶节点进行操作。
{
    if(x!=father[x])
    {
        int tmp;
        tmp=find(father[x]);
        under[x]+=under[father[x]];
        father[x]=tmp;
    }
    return father[x];
}
void Union(int x,int y)//合并树。
{
    int root1=find(x);
    int root2=find(y);
    if(root1!=root2)
    {
        under[root1] = count[root2];          //将X放在Y的上方。此次并查集所建立的树为倒置的树。下端为根,及为root1。因此root1的权值(下方个数)即为Y的总和。
        count[root2] += count[root1];         //root2变为了root1的根,而root2的和即为原本root2+root1的和
        father[root1] = root2;                 //改变X堆的父节点。之前为自己或者原来的父节点。之后改变为Y堆的父节点。
    }
}


int main()
{
    int i,j;
    int k;
    char s[5];
    while (scanf("%d",&N)!=EOF)
    {
        init();
        while(N--)
        {   scanf("%s",s);
            if (s[0]=='M')
            {
                scanf("%d%d",&i,&j);
                Union(i,j);
            }
            else
            {
                scanf("%d",&i);
                find(i);//为什么做这步呢?是因为Union合并后,原树的子节点仍然保留原来根的值。而且权值还未改变。需要时需要重新查找
                printf("%d\n",under[i]);
            }
        }
    }
    return 0;
}



TJu3732 Dragon Balls


题目描述:悟空在寻找龙珠,一共有n个龙珠,m条操作。操作有两种。


T a b 表示把a龙珠所在的城里的所有龙珠运到b所在的城里


Q a 表示对a的询问,要求输出  x: a龙珠所在的城,y: a龙珠所在的城里一共有多少个龙珠, z: a龙珠经过几次到达现在所在的城的。


分析:定义father同并查集的一般操作,step表示经过几步到达现在所在的城,count表示该城里的龙珠数。*/
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
int father[10005];
int count[10005];
int step[10005];
void init()//此处试了一下不传参数优化也可以,数据不大
{
    for(int i=1;i<=10005;i++)
    {
        father[i]=i;
        count[i]=1;
        step[i]=0;
    }
}
int find(int x) //Union调用时,更新被作为根的那棵树的所有节点值,而不更新A树除根节点外的节点值。
{
    if(x!=father[x])
    {
        int tmp=father[x];  //递归的过程,更新
        father[x]=find(tmp);
        step[x]+=step[tmp];
    }
    return father[x];
}
void Union(int x,int y)  //合并树的过程
{
    int root1=find(x);   //在每一次找此树的根节点时,因为调用了find函数及将之前的树的分支也进行了操作。但是合并后的树并未进行操作
    int root2=find(y);
    father[root1]=root2;
    count[root2]+=count[root1];
    count[root1]=0;      //这步加不加无所谓。但是实际上root1节点的龙珠被拿走,节点count为空。
    step[root1]++;       //树合并完成后,根节点已经连接在B树上操作完成。且find函数里更改已经结束,因此在原值上+1即可。(A树上的除根节点外的其他值并未发生改变)
}
int main()
{
   int T,t=1;
   cin>>T;
   char s[5];
   while(T--)
   {
        printf("Case %d:\n",t++);
       int N,Q;
       int a,b;
       cin>>N>>Q;
       init();
       while(Q--)
       {
       scanf("%s",s);
       if(s[0]=='T')
       {scanf("%d%d",&a,&b);
           Union(a,b);
       }
       else
       {
           scanf("%d",&a);
           int t;
           t=find(a);
           printf("%d %d %d\n",t,count[t],step[a]);
       }
       }
   }
   return 0;
}


如上面两道题所表达的是一个关系数组或多个关系数组,分别存放并查集的节点中的值。并进行查询。
2.  进一步的带权并查集(种类相关并查集)
    这类题目中不仅会有信息,不单纯只是让我们整理输出这些信息,而是进一步就判断这些信息的关系。
    大体分为:数字关系(判断存储值),逻辑关系(判断对错)->找出对应关系(分帮派,昆虫真爱判断)
    下面先给出http://blog.csdn.net/iaccepted/article/details/21233745博客的一段文字表述:
    分组并查集->偏移向量实现
    题目如下题:可以分别建立两个并查及,开辟双倍的空间以及多做一次查找。
    然而如果数据量大的话,也不知道能不能过。所以保险起见分享下面方法:偏移向量实现
    数学逻辑很简单,我这数学渣渣都能看懂。
    
    由于只有两组,所以偏移向量只要记录0或者1就可以了,当然这里如果不取余直接记录真实的偏移量也是可以的,
    但在最后查找判断的时候也少不了取余判断这一步。查找的时候更新偏移量是很好理解的,直接就是向量加然后取余,
    a->fa(a的根节点时fa),那么在查找的时候,要把a直接连接在fa的父节点上的,这个时候根据向量关系就可以得到
    relation[a] = relation[a] + relation[fa],这里relation[a]表示从a到其根节点的偏移量,所以在这里查找的之前要先把a的根节点保存下来,
    因为路径压缩的时候其根节点就变成其新的根节点了。当然因为本题只有两组,所以加个取余就可以了。然后就是合并的时候的更新问题,
    在合并的的时候本题就是两组即要合并的两点属于不同的组,即如果a在relation为0的组那么b就在relation为1的组,反之亦然,所以他们的偏移量差就是1,
    那么在合并的时候就要利用向量运算来合并了,假设
    a->fa,b->fb(a的根节点是fa,b的根节点是fb),现在要合并a、b,我们再假设把a的父节点合并到b的父节点上也就是pre[fa] = fb;
    根据向量运算fa->fb =  fa->a + a->b +b->fb,这样就能推导出偏移更新式(2-relation[fa] + 1 + relation[b])%2,化简一下就是(1+offset[b]-offset[a])%2,ok了,这样就解决掉了。
    个人感觉靠左原则更好一些。然而网上代码却全部靠右。
    靠左合并只是需要将fa,fb对调即可。
 

HDU1829 A Bug's Life
给出K值,下面有K个测试。输入n,m:n为昆虫个数,m为给定对数。
如果x,y关系相同则为同性恋,否则异性是允许的,对应两种不同输出。
#include <iostream>
#include <stdio.h>
using namespace std;
int relation[5000];
int father[5000];
int flag=1;
void init(int x)
{
    for(int i=1; i<=x; i++)
    {
        father[i]=i;
        relation[i]=0;
    }
    flag=1;
}


int find(int x){
    int px;
    if(x!=father[x])
    {
        px = father[x];
        father[x] = find(father[x]);
        relation[x] = (relation[px] + relation[x])%2; 
    }
    return father[x];
}
void Union (int x,int y)
{
    int root1=find(x);
    int root2=find(y);


    if(root1!=root2)
    {
        father[root2]=root1;
        relation[root2]=(relation[x]-relation[y]+1)%2;//注意此步是x , y 传参的节点值的+ - ,不是根值
    }
    find(y);//此步仍然是节点,因为连接后,加上去的节点并未改变.  这两步找错找了两个小时T-T;网上的fx x fy y 好难看
}
int main()
{
    int i,j,k,t;
    scanf("%d",&k);
    for(i=1;i<=k;i++)
    {   int n,m,x,y;
        scanf("%d%d",&n,&m);
        init(n);
        for(j=1;j<=m;j++)
        {
            scanf("%d%d",&x,&y);
            if(flag)
            {
                Union(x,y);
            }
            if(relation[x]==relation[y])
                flag=0;
        }
        printf("Scenario #%d:\n",i);
        if(flag){
            printf("No suspicious bugs found!\n");
        }else{
            printf("Suspicious bugs found!\n");
        }
        cout<<endl;
    }
    return 0;
}


poj 1703 Find them, Catch them (分组并查集 偏移向量实现)

题目大意:

给定t组数据,给出n,m:n个人,m个操作:

操作分两类:

A:查询 : 输入x,y,查询二者关系。

B:增加: 输入x,y, 合并二者所在团伙。

三类输出 : 无法确定,同伙,不同伙。

分析:father数组存放父节点,relation存放与父节点关系。

注意:根相同表明二者已经在同一并查集中可以查询,relation 0 / 1 分别代表了二者关系;

#include <iostream>
#include <stdio.h>
using namespace std;
int father[100010];
int relation[100010];
void init(int n)
{
    for(int i=0;i<=n;i++)
    {
        father[i]=i;
        relation[i]=0;
    }
}
int find(int x)
{
    if(father[x]!=x)
    {
        int tmp=father[x];
        father[x]=find(father[x]);
        relation[x]=(relation[x]+relation[tmp])%2;
    }
    return father[x];
}
void Union(int x,int y)
{
    int root1=find(x);
    int root2=find(y);
    if(root1!=root2)
    {
        father[root2]=root1;
        relation[root2]=(relation[x]-relation[y]+1)%2;
    }
}
int main()
{
    int t,n,m,x,y,i,j;
    scanf("%d",&t);
    char s[5];
    while(t--)
    {
        scanf("%d%d",&n,&m);
        init(n);
        while(m--)
        {
        scanf("%s%d%d",s,&x,&y);
        if(s[0]=='A')
        {
            int fa=find(x),fb=find(y);
            if(fa==fb)
            {
                if(relation[x]==relation[y])
                {
                    printf("In the same gang.\n");
                }
                else
                    printf("In different gangs.\n");
            }
            else
                printf("Not sure yet.\n");
        }
        else
        {
            Union(x,y);
        }
        }
    }
    return 0;
}


具体实现同上,不做过多注释了!

以上都是0 1 关系可以实现的。下面给出一个存储三种逻辑关系的题。

POJ 1182 食物链

题意给出N个数字,表示N个动物,K个测试例子,之后按d x y 输入。分别代表X与Y的关系。
分析:
    看到题目会思考如果建立并查集,一定会考虑到里面存在 吃 / 被吃 / 同类 的关系,因此是个带权的种类并查集。
   (靠左合并) 三种关系 一定需要三种不同的值来存储,因此此题中relation 中分为三类 0 同类 ; 1 被根节点吃掉;2 吃掉根节点  
    向量偏移法解题如下:

#include <iostream>
#include <stdio.h>
using namespace std;
int father[50010];
int relation[50010];
int ans=0;
int find(int x)
{
    if(father[x]!=x)
    {
        int tmp=father[x];
        father[x]=find(father[x]);
        relation[x]=(relation[x]+relation[tmp])%3;
    }
    return father[x];
}
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    int i;
    for(i=0;i<=n;i++)
    {
        father[i]=i;
        relation[i]=0;
    }
    ans=0;
    int d,x,y;
    while(k--)
    {
        scanf("%d%d%d",&d,&x,&y);
        if(x>n||y>n)
        {
            ans++;
            continue;
        }
        //时刻注意合并是根的操作, 查找是节点的操作
        int root1=find(x);
        int root2=find(y);
        //只有根相同时才可以判断对错,如果不同时,会执行合并步骤,不会有对错的考虑
        if(root1==root2)
        {
            if((relation[y]+3-relation[x])%3!=d-1) 
            {
                ans++;
                continue;
            }// y - > x
        }
        //根相同的判断: root1=root2:  查找relation[y] 与relation[x]的关系
        // y - > x : Y->root2 root1 -> X : relation[y] +  ( 3 - relation [x]) 是否等于d - 1 
        // d - 1 : 我们给出的是x与y的关系,同类,吃Y : 那么对应Y存储的就应该是  同类 0 被吃 1
        if(root1!=root2)
        {
            father[root2]=root1;
            relation[root2]=(3-relation[y]+d-1+relation[x])%3;
        }//root2- > y + y-> x + x- > root1
        //根不同的操作:
    }
    printf("%d\n",ans);
    return 0;
}
3.并查集删除节点的操作:

当我们需要删除并查集中某个节点的时候,我们自然不能直接将他们踢出集合,否则将会影响连接在他们上面的子节点的根值。

解决办法:

将所有的节点进行编号,在删除的时候,将要删除节点的编号删除,而并不改变其物理存储结构,对于这个删除出去或者要移动的节点,我们将其插入到整体的尾端,并且重新进行编号。

例题及代码

带权并查集--删除--UVA11987

#include <iostream>
#include <stdio.h>
using namespace std;
int father[200010];
int cnt[200010];
int sum[200010];
int id[200010];
int tot,n,m;
void init(int n)
{
    for(int i=1; i<=n; i++)
    {
        father[i]=i;
        sum[i]=i;
        cnt[i]=1;
        id[i]=i;
    }
}
int find(int x)
{
    if(father[x]!=x)
        father[x]=find(father[x]);
    return father[x];
}
void del(int x)
{
    int tmp=find(x);
    sum[tmp]-=x;
    cnt[tmp]--;
    tot++;
    father[tot]=tot;
    cnt[tot]=1;
    sum[tot]=x;
    id[x]=tot;
}
void Union(int x,int y)
{
    int root1=find(x);
    int root2=find(y);
    if(root1!=root2)
    {
        cnt[root1]+=cnt[root2];
        father[root2]=root1;
        sum[root1]+=sum[root2];
    }
}
int main()
{
    while(~scanf("%d%d",&n,&m))
    {
        init(n);
        tot=n;
        while(m--)
        {
            int op,x,y;
            scanf("%d",&op);
            if(op==1)
            {
                scanf("%d%d",&x,&y);
                Union(id[x],id[y]);//合并id,而非直接值合并
            }
            else if(op==2)
            {
                scanf("%d%d",&x,&y);
                if(find(id[x])!=find(id[y]))
                {
                    del(x);
                    //del(id[x])这里不行。因为要保持数的原值。并且del中进行反复操作
                    Union(id[x],id[y]);
                }
            }
            else
            {
                scanf("%d",&x);
                int tmp=find(id[x]);
                printf("%d %d\n",cnt[tmp],sum[tmp]);
            }
        }
    }
    return 0;
}





  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值