题目描述
有三类动物ABC,如图 1-1所示,A吃B,B吃C,C吃A。给出N个动物和K个动物之间的关系,判断有多少局是假的,即与其他的话有明显的矛盾。例:A吃A与B吃A,则为明显的矛盾。
图 1-1 食物链关系描述
算法设计
使用带权并查集(如果对并查集不了解,可以参考并查集),将确定了关系的节点储存在一棵树中,没有确定关系则在不同的树中。权重表示该节点与父节点的关系,如对于动物X和动物Y,假如在树中X为Y的父亲。
由图 1-1可知,X和Y的关系只有以下三种
- 如果XY为同类,则Y对应的权重为0
- 如果X吃Y,则Y对应的权重为1
- 如果Y吃X,则Y对应的权重为2
关系更新如下所示
- 查找时关系更新
假设查找前后如图 1-2所示,根据z与x的关系不变,则有关系式rz’=(ry+rz)%3。
图 1-2查找关系更新图解 - 合并时关系更新
如图 1-3所示,记fx为x的父节点,fy为y的父节点,根据合并前后关系不变则有(rfy+ry) mod 3=(rx+r-1) mod 3即rfy=(rx+r-ry+2) mod 3,这里加2的原因是采用0、1、2表示关系,输入r则为1、2、3.
图 1-3 合并关系更新图解 - 判断是否满足关系
如果输入的两个元素已经在同一个不相交集中,那么需要判断输入与之前的关系是否产生矛盾,如图1-4所示,根据关系不会改变的特性,可以推知关系式为rx+r-1=ry,即(r-1)==(r[y]-r[x]+3)%3,根据该关系进行判断。
图 1-4 判断关系图解
伪代码描述
find(a)
if(a != father[a])
t = father[a]
father[a] = find[t]
r[a] = (r[a] + r[t]) % 3
return father[a]
uinon(a,b,r)
fa = find(a)
fb = find(b)
father[fb] = fa
r[fb] = (r[a] + r – r[b] + 2)%3
judge(r,x,y)
if(x > N || y > N){
count ++
}
else if(x == y && r != 1){
count ++
}
else{
fx = find(x)
fy = find(y)
if(fx == fy){
if(r – 1 != (r[y] – r[x] + 3 )%3){
count ++}
else{
union(x,y)
}
}
代码实现
//
// Created by kiff on 17-11-19.
//
#include <stdio.h>
int father[50001];
/*the relation to father
0: the same species
1: father eats i
2: i eats father*/
int relation[50001];
int find(int search);
void Union(int a,int b,int r);
int main(){
//initial the union find
for (int i = 0; i < 50001; ++i) {
father[i] = i;
relation[i] = 0;
}
int N,K,d,x,y;
scanf("%d%d",&N,&K);
int count = 0;
for (int i = 0; i < K; ++i) {
scanf("%d%d%d",&d,&x,&y);
if(x > N || y > N){//the input is illicit
count ++;
}
else if(x == y && d != 1){
count ++;
}
else{
int fx = find(x);
int fy = find(y);
if(fx == fy){
//judge if this answer is conflicted with others
if ((d-1) != (relation[y] - relation[x] + 3)%3){
count ++;
}
} else{//union the two elements
Union(x,y,d);
}
}
}
printf("%d",count);
}
int find(int search){
if(father[search] != search){
int temp = father[search];
father[search] = find(temp);
//update the realtion between search and its father
relation[search] = (relation[search] + relation[temp]) % 3;
}
return father[search];
}
void Union(int a,int b,int r){
int father_a = find(a);
int father_b = find(b);
father[father_b] = father_a;
/*update the relation between father_b and father_a
because I use 0 1 and 2, however the input is 1 2 and 3,so I plus 2 rather than 3 to ensure it is non-negative*/
relation[father_b] = (relation[a] + r - relation[b] + 2)%3;
}
复杂度分析
假设输入规模为n,输入数量为m,则构建并查集的复杂度为O(n),如果输入直接非法,则可以在O(1)的时间内发现,所以不需要考虑,如果和前面的由冲突那么通过find操作可以进行判断,前面说到find的摊还复杂度为α(n),如果不在同一个不相交集合,那么可以使用union进行合并,union的摊还复杂度为α(n),所以整个程序的复杂度为n+mα(n)。
编程技巧
由于之前接触到的并查集不多,且均是非带权并查集,看到该题目只是想到了添加一个辅助数组来进行关系的划分,但是不清楚如何进行表示,看了相关题解后发现可以使用0、1、2这三个数字分别表示该关系,然后进行求解,由于循环关系很方便的表示和进行判断。此外,在初始化的时候使用memset将所有节点都置为-1,比使用循环效率要高。