描述
动物王国中有三类动物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),输出假话的总数。
输入
第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。
输出
只有一个整数,表示假话的数目。
样例输入
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
样例输出
3
两种搞法。
第一种是扩展域并查集,这种并查集适用于变量间关系不止一种,比如除了等于,还有不等于的关系需要并起来。
或者是奇偶性,同性,或者异性,连续2次异性等于同性。
第二种是带权并查集,这种适用于可以赋值权值的并查集。合并集合的时候利用矢量和进行链接2个集合的边权关系。
这一题有3中关系,同类,吃,被吃。
(一条链上)连续吃3次(相当于连续集合并三次)等于同类,连续被吃3次等于同类,连续吃两次等于天敌,连续被吃两次等于猎物。上面的关系有的绕 用A->B->C->D来说,链上连续吃,那么D一定与A 是同类。连续被吃A,D也一定是同类。
如果有一个信息,A,B同类,那么如果A吃B 或者B吃A,这个信息都是错的。
如果有一个信息,A吃B,那么如果A.B同类 或者B吃A,这个信息都是错的。
以上判断条件,不重不漏。那我们如何去做呢?
第一种。
扩展域并查集,我们为每个动物都开3个域,同类域self,捕食域eat,天敌域enemy
先说合并集合
如果A,B同类,那不用说,三个域肯定都相同。
如果A吃B,那么A的捕食域与B的同类域并起来,A的捕食域与B的天敌域并起来,A的天敌域与B的捕食域并起来。
那么如果2个动物A,B A的捕食域与B的同类域在同一集合,代表A吃B,
这样搞我们就能进行之前说的判断了。
还有一个问题,为啥保证链能连上。
你想啊,如果A吃B,B吃C,C吃D,(如果两个物种同类只是把三个域平行连不交叉,不影响结果)
A同类域连B天敌域(代表了 B的天敌是与A是一类), B天敌域连C捕食域(代表了 B的天敌和C的捕食是一类 ),C捕食域连D同类域(代表了 C的捕食与D的同类,是一类)链式连的都是一类动物,说明A,B的天敌,C的捕食,D是一类,正好满足了捕食成环的关系,是不是很巧妙 多想想自己敲一遍就明白了。
#include<bits/stdc++.h>
using namespace std;
const int M = 500000+100;
int fa[M],eat[M];
int get(int x)
{
if(fa[x]==x)return x;
return fa[x]=get(fa[x]);
}
int main()
{
int n,k;
cin>>n>>k;
for(int i=1;i<=n+n+n;i++)
fa[i]=i;
int cnt=0;
int ec=0;
while(k--)
{
int d,x,y;
scanf("%d %d %d",&d,&x,&y);
if(x>n||y>n||(d==2&&x==y))
{
cnt++;
continue;
}
int gx_self=get(x),gx_eat=get(x+n),gx_enemy=get(x+n+n);
int gy_self=get(y),gy_eat=get(y+n),gy_enemy=get(y+n+n);
if(d==1)
{
if(gx_self==gy_eat||gx_eat==gy_self)
{
cnt++;
continue;
}
fa[gx_self]=gy_self,fa[gx_eat]=gy_eat,fa[gx_enemy]=gy_enemy;
}
else
{
if(gx_self==gy_self||gy_eat==gx_self)
{
cnt++;
continue;
}
fa[gx_self]=gy_enemy,fa[gx_eat]=gy_self,fa[gx_enemy]=gy_eat;
}
}
printf("%d\n",cnt);
return 0;
}
另一种就是带权并查集 本质是一样,不过在合并集合和判断的适合有些区别。
基本套路是,边权维护与父亲节点的关系,然后合并集合的时候(假设让x的祖先的父节点变成y的祖先)用矢量和计算法则 根据x,y的边权 计算x的祖先应该被赋予的值。(每个集合祖先的边权都是0,给祖先赋值是为了把2个集合之前差的关系传递(通过路径压缩)给子孙)。然后如果x,y都在一个集合里就可以直接根据x,y的边权来进行判断了。
这一题我们可以让边权为与父亲吃或被吃的 关系。
当边权==0,与父亲同类;
当边权==1,吃父亲;
当边权==2,被父亲吃;
每个集合的祖先默认边权为0;
那么子孙与父亲的关系,通过路径压缩时,从上往下,的边权等于自身加上父亲的边权,d[x]=(d[fa[x]]+d[x])%3;
因为,
当d[fa[x]]=1,d[x]=0,时,x的父亲吃x的爷爷,x与父亲同类,那x就吃父亲。d[x]=1;
……1,……1,x的父亲吃x的爷爷,x吃父亲,那x就被爷爷吃。d[x]=2;(别忘了,只有三个物种环状吃与被吃)
……0,……1,x的父亲与x的爷爷同类,x吃父亲,那x就吃爷爷。d[x]=1;
……1,……2,x的父亲吃x的爷爷,x被父亲吃,那x与x的爷爷同类。(都被x的父亲吃)d[x]=0;
……2,……1,x的父亲被x爷爷吃,x吃父亲,那x与爷爷同类(都吃x的父亲)d[x]=0;
……2,……2,x的父亲被x的爷爷吃,x被父亲吃,那x吃x的爷爷(相当于倒着环吃)d[x]=1;
就上面6种情况,搞明白一次以后再做带权并查集就好理解了。
#include<bits/stdc++.h>
using namespace std;
const int M = 100000+100;
int fa[M],d[M];
//d[]==1吃父亲,d[]==2被父亲吃,d[]==0和父亲同类
int get(int x)
{
if(fa[x]==x)return x;
int root = get(fa[x]);
d[x]=(d[fa[x]]+d[x])%3;//路径压缩从祖先开始向下传递
return fa[x]=root;
}
int merge(int D,int x,int y)
{
int gx=get(x),gy=get(y);
if(gx==gy)
{
// printf("%d----%d %d %d\n",d[x],d[y],gx,gy);
if((d[x]-d[y]+3)%3!=D-1)
return 1;
return 0;
}
fa[gx]=gy;
d[gx]=(D-1+d[y]-d[x]+3)%3;//为了让x所在集合的点的边权都加上x,y集合的边权矢量差。
// puts("oo");
return 0;
}
int main()
{
int n,k;
cin>>n>>k;
for(int i=1;i<=n;i++)
fa[i]=i,d[i]=0;
int cnt=0;
while(k--)
{
int D,x,y;
scanf("%d%d%d",&D,&x,&y);
int gx=get(x),gy=get(y);
if(D==2&&x==y||x>n||y>n||merge(D,x,y))
{
// puts("1111");
cnt++;
continue;
}
}
printf("%d\n",cnt);
return 0;
}