<(扩展域/加权)并查集>NOI 2001 食物链

可提交的传送门

题目描述 Description

动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。A吃B,B吃C,C吃A。   
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。   
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是“1 X Y”,表示X和Y是同类。   
第二种说法是“2 X Y”,表示X吃Y。   
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。  
1) 当前的话与前面的某些真的话冲突,就是假话;  
2) 当前的话中X或Y比N大,就是假话;   
3) 当前的话表示X吃X,就是假话。   
你的任务是根据给定的N(1<=N<=50,000)和K句话(0<=K<=100,000),输出假话的总数。

输入描述 Input Description

第一行是两个整数N和K,以一个空格分隔。   
以下K行每行是三个正整数D,X,Y,两数之间用一个空格隔开,其中 D 表示说法的种类。   
若D=1,则表示X和Y是同类。   
若D=2,则表示X吃Y。

输出描述 Output Description

只有一个整数,表示假话的数目。

样例输入 Sample Input

100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5

样例输出 Sample Output

3

数据范围及提示 Data Size & Hint

输入文件
对7句话的分析 100 7
1 101 1  假话
2 1 2   真话
2 2 3   真话
2 3 3   假话
1 1 3   假话
2 3 1   真话
1 5 5   真话


Solution 1:加权并查集

  • 权值
    用rank数组表示:
    对于任意一个集合:
    1.根节点权值为0
    2.吃根节点的权值为2
    3.被根节点吃的权值为1
    注意:在未路径压缩时,子节点的权值表示的是和它父亲的关系
  • find操作
    对于普通的并查集,我们可以直接一路找到根节点实现路径压缩。但是加权并查集不能这样啊。
    这里写图片描述
    对于这样一次查找操作,目前,rank[x]的值是相对于fa[x]的关系。我们要把x合并到fa[fa[x]]上去,就要改变rank[x]的值。
    由于三种物种组成的关系呈三角形,我们可以用向量的思维来思考这个问题。
    先来直观感受一下,如图,假设rank[x]==1,rank[fa[x]]==2,rank[fa[fa[x]]]==0
    rank[x]==1说明fa[x]吃x,而rank[fa[x]]==2说明fa[x]吃fa[fa[x]],所以x和fa[fa[x]]是同类,所以当x指向fa[fa[x]]时,rank[x]=0 。
    其实不难发现rank[x]=(rank[x]+rank[fa[x]])%3 ,通过向量,这个关系式显然。

  • merge操作
    对于两个并查集的合并,不仅要改变fa数组的值,还要改变rank[fa[x]]
    这里写图片描述
    如图,要合并x所在的集合和y所在的集合。
    对于已经输入的语句: (设合并后y的权值为rank[y]’)
    1.如果x和y是同类即d==1。也就是说合并后,y指向fa[x]时的rank[x]==rank[y]’ ①,也就是图中的蓝色线应等于x指向fa[x]的线。
    由向量相加可得:
    (rank[y]+rank[fa[y]])%3=rank[y]’②;
    联立①②两式可得:(rank[y]+rank[fa[y]])%3=rank[x]
    等式变形可得:rank[fa[y]]=(rank[x]-rank[y])%3;
    又因为在C++中,模运算有可能得到负数,所以最后的赋值语句就是:rank[fa[y]]=rank(rank[x]-rank[y]+3)%3;
    2.如果x吃y即d==2。因为对于吃与被吃的关系,如果x吃y,前面已经得出,合并后y和x的关系应为:rank[y]’=(rank[x]+1)%3①。同样根据向量的知识得:(rank[y]+rank[fa[y]])%3=rank[y]’②
    联立①②得:(rank[y]+rank[fa[y]])%3=(rank[x]+1)%3
    等式变形得:rank[fa[y]]=(rank[x]-rank[y]+1)%3
    同样防止变为负数,再加3:
    rank[fa[y]]=(rank[x]-rank[y]+1+3)%3
    综上:每次合并只改变合并前集合的父节点的rank值,后面的子节点就在路径压缩中自己更新了

  • 对于本题:
    1.如果输入的语句表示x > n或y > n或 x吃x 直接ans++
    2.如果两个不在一个集合里,肯定是真话,并且合并集合
    3.如果两个在同一个集合里,判断两者是否符合读入的关系,如果不符合,ans++
    至于如何判断在同一个集合里的两个节点是否符合关系:
    很简单:
    1)如果d==1,x和y是同类,查看是否rank[x]==rank[y]
    2)如果d==2,x吃y,查看是否rank[y]==(rank[x]+1)%3
    代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int maxn=100000+50;
int n,k,ans;
int fa[maxn],rank[maxn];

int find(int x)
{
    if(fa[x]==x) return x;
    int f=fa[x];
    fa[x]=find(fa[x]);
    rank[x]=(rank[x]+rank[f])%3;
    return fa[x];
}
bool check(int d,int x,int y)
{
    int f1=find(x),f2=find(y);
    if(f1==f2)
    {
        if(d==1)
        {
            if(rank[x]==rank[y]) return true;
            return false;
        }
        else
        {
            if(rank[y]==(rank[x]+1)%3) return true;
            return false;
        }
    }
    else
    {
        fa[f2]=f1;
        if(d==1) rank[f2]=(rank[x]-rank[y]+3)%3;
        else rank[f2]=(rank[x]-rank[y]+1+3)%3;
        return true;
    }
}
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;++i) fa[i]=i;
    for(int i=1;i<=k;++i)
    {
        int d,x,y;
        scanf("%d%d%d",&d,&x,&y); 
        if(x>n||y>n||(d==2&&x==y)) ans++;
        else if(!check(d,x,y)) ans++;
    }
    printf("%d",ans);
    return 0;
}

其实有一个巧妙的改变,可以节省代码量:
观察:
1.合并操作中:
d==1时,rank[fa[y]]=(rank[x]-rank[y]+3)%3
d==2时,rank[fa[y]]=(rank[x]-rank[y]+1+3)%3
其实直接可以省略为:rank[fa[y]]=(rank[x]-rank[y]+d-1+3)%3
即:rank[fa[y]]=(rank[x]-rank[y]+d+2)%3
2.验证关系是否符合时:
d==1时,rank[x]==rank[y]
d==2时,rank[y]==(rank[x]+1)%3
可以直接省略为:rank[y]==(rank[x]+d-1)%3
所以check函数就可以这样写了:

bool check(int d,int x,int y)
{
    int f1=find(x),f2=find(y);
    if(f1==f2)
    {
        if(rank[y]==(rank[x]+d-1)%3) return true;
        return false;
    }
    else
    {
        fa[f2]=f1;
        rank[f2]=(rank[x]-rank[y]+d+2)%3;
        return true;
    }
}

Solution 2:扩展域并查集

三倍空间
fa[x] x的同类域
fa[x+n] 吃域
fa[x+2*n] 被吃域
每次合并或查询时直接查询就可以了。
就是有点麻烦,很容易混,多想想就好了
不多说
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int maxn=50000+10;
int n,k,ans;
int fa[maxn*3];

int find(int x)
{
    if(fa[x]==x) return x;
    return fa[x]=find(fa[x]);
} 
bool check(int d,int x,int y)
{
    int x1,x2,x3,y1,y2,y3;
    x1=find(x);//同类域 
    x2=find(x+n);//吃域 
    x3=find(x+(n<<1));//被吃域 
    y1=find(y);
    y2=find(y+n);
    y3=find(y+(n<<1));
    if(d==1)
    {
        if(x2==y1||x1==y2) return false;
        fa[x1]=y1;
        fa[x2]=y2;
        fa[x3]=y3;
        return true;
    }
    else
    {
        if(x3==y1||x1==y1) return false;
        fa[x1]=y3;
        fa[x2]=y1;
        fa[x3]=y2;
        return true;
    }
}
int main()
{
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n*3;++i) fa[i]=i;
    for(int i=1;i<=k;++i)
    {
        int d,x,y;
        scanf("%d%d%d",&d,&x,&y);
        if(x>n||y>n||(d==2&&x==y)) ans++;
        else if(!check(d,x,y)) ans++;
    }
    printf("%d",ans);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值