种类并查集与带权并查集

种类并查集

裸题:食物链
题意
  在一个生态系统存在一些食物链,这些食物链满足:A吃B,B吃C,C吃A,给出一些关系。问这些关系(A、B同类或A吃B)中假话有多少(按照先后顺序,与前面不矛盾就是真话)
思路
  很明显我们不管从哪一方考虑,每一种生物最多有三种角色,我吃别人,或者别人吃我,即存在捕食,和天敌,再加上自身,一共三种角色。
  所以,为了很好的表示这些关系,我们要对同一种生物进行分身,在并查集里面开三倍数组就行,分别表示这三种角色。
  之后,就是对这些关系进行处理了

  1. A和B是同类:因为每个并查集都表示的是同一种生物的某一类型,说到底还是同一种生物,所以要对三个并查集分别进行合并操作,ff[find(A)]=ff[find(B)];
  2. A吃B:也要处理三个并查集,因为A吃B已经说明三个角色的顺序为A->B->C->A,对于A来说,A捕食B,对于B来说,B的天敌是A,对于C这个中间节点,即要表示C吃A,换句话说B捕食的生物是A的天敌。如果另第一个并查集表示本身,第二个并查集表示捕食的是什么,第三个并查集表示天敌是什么,则有如下表示:ff[A+n]=ff[B],ff[B+2n]=ff[A],ff[B+n]=ff[A+2n].
#include<bits/stdc++.h>
using namespace std;
#define maxn 50005
#define maxm 500005
#define MOD 31011
#define ll long long
#define inf 2e9

int ff[3*maxn];
int find(int x)
{
    return x==ff[x]?x:ff[x]=find(ff[x]);
}

void Union(int x,int y)
{
    int nx=find(x),ny=find(y);
    if(nx!=ny)
        ff[nx]=ny;
}

int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1; i<=3*n; i++)ff[i]=i;

    int ans=0;
    for(int i=0; i<m; i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);

        if(b>n||c>n)
        {
            ans++;
            continue;
        }

        if(a==1)
        {
            if(find(b)==find(c+n)||find(c)==find(b+n)||find(b+n)==find(c+2*n)||find(b+2*n)==find(c+n))
            {
                ans++;
                continue;
            }
            Union(b,c),Union(b+n,c+n),Union(b+2*n,c+2*n);
        }
        else if(a==2)
        {
            if(find(b)==find(c)||find(c+n)==find(b))
            {
                ans++;
                continue;
            }
            Union(b+n,c),Union(c+2*n,b),Union(c+n,b+2*n);
        }
    }

    printf("%d\n",ans);
    return 0;
}


带权并查集

裸题:洛谷,银河英雄传说
题意
  对于1到n的序列,有两种操作:1、Mij,将i所在的队列加到j所在的队列中, 当然是从后面加,保证原队列的顺序不变。2、Cij,查找i和j之间有多少个数,当然如果ij不在一个队列中,输出-1就行。
思路
  有队列合并,队列查询,并查集肯定少不了,但是如何求i和j有多少个数,这就要用到带权并查集了,另外开一个数组记录每个点到根节点之间的距离,len数组吧,最终答案就是len[i]+len[j]-1,记得减一因为不包含两个端点。
  这个带权并查集怎么写,很明显假如不用路径压缩,合并一次对这条链进行更新,还有查询是否在一个集合也是从头找到尾,这样的话会超时,复杂度是O(n^2),n为3万,(实测路径压缩+记录路径,刚好能卡过,这样常数比较小而已,但如果加强数据上限还是会卡死的,有点玄(4e8的样子))
  最终正解就是,路径压缩,每次合并对当前根节点(主要不是合并后的根节点)进行修改,合并,当前根节点的距离=要合并队列的长度。但是有个问题,当前队列后面节点到根节点的距离并没有变,还是原来的,这只是对当前根节点进行了修改。这个问题在find函数中解决,每次find就对这个点到根节点的距离进行更新(条件是这个点是第一次find,也就是这个时候他的路径还没有被压缩,即ff[x]!=ff[ff[x]],当然如果是第二次find,路径已经压缩过了,他到根节点的距离是不会变的),len[x]+=len[ff[x]],其实就是把刚刚对根节点增加的长度分配到每个子节点上(条件是使用find时,不使用find的子节点其实并没有分配,这也不影响最终的结果,就相当于按需分配

//AC代码 :带权并查集
#include<stdio.h>
#include<cmath>
using namespace std;
#define IOS ios::sync_with_stdio(false)
#define maxn 30005
#define INF 1000000005
#define ll long long

//dis表示i队列的长度,len表示i到根节点的距离
int ff[maxn],dis[maxn],len[maxn];

int find(int x)
{
    if(ff[x]==x)
        return x;
    int t=find(ff[x]);
    len[x]+=len[ff[x]];
    return ff[x]=t;
}

void read(int &x)
{
    x=0;
    bool flag=0;
    char ch=getchar();
    if(ch=='-') flag=1;
    while(ch<'0'||ch>'9')ch=getchar();
    while(ch>='0'&&ch<='9')x*=10,x+=ch-'0',ch=getchar();
    if(flag) x=-x;
}

int main()
{
    int n;
    read(n);
    for(int i=0; i<=maxn; i++)
        ff[i]=i,dis[i]=1;

    for(int i=0; i<n; i++)
    {
        char str;
        scanf(" %c",&str);
        int x,y;
        read(x),read(y);
        int nx=find(x);
        int ny=find(y);
        if(str=='M')
        {
            if(nx!=ny)
            {
                ff[nx]=ny;
                len[nx]+=dis[ny];
                dis[ny]+=dis[nx];
                dis[nx]=1;
            }
        }
        else
        {
            if(nx!=ny)
            {
                printf("-1\n");
                continue;
            }
            if(len[x]>len[y])
                printf("%d\n",len[x]-len[y]-1);
            else
                printf("%d\n",len[y]-len[x]-1);
        }
    }
    return 0;
}

发布了41 篇原创文章 · 获赞 2 · 访问量 1255

猜你喜欢

转载自blog.csdn.net/qq_41418281/article/details/100119887