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
第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。
Output
只有一个整数,表示假话的数目。
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
solution1
我们把它们之间的关系转化成模3向量,A吃B, B吃C,C吃A,就是
A
B
⃗
=
B
C
⃗
=
C
A
⃗
=
1
\vec{AB}=\vec{BC}=\vec{CA}=1
AB=BC=CA=1,
A
C
⃗
=
2
\vec{AC}=2
AC=2 就表示C吃A。
三类关系,假设x的父亲是y,y->x:
- 0 同类
- 1 y吃x
- 2 x吃y
然后我们看各个情况的传递关系:(图中的含义大家对应着翻译过来就很清晰了)
下图分别对应code1中的46行和57行。
code1
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int f[N]; //f[i]存点i的父节点编号
int r[N]; //r[i]存点i与父节点的关系,0,1,2
int findd(int x)
{
if (f[x] == x)
return x;
int t = f[x];
f[x] = findd(f[x]); //每次压缩路径前把原来的父子结点对应的关系r更新
r[x] = (r[x] + r[t]) % 3;
return f[x];
}
void mergee(int d, int x, int y)
{
int xx = findd(x), yy = findd(y);
f[yy] = xx;
r[yy] = (r[x] + (d - 1) - r[y] + 3) % 3;
}
int main()
{
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
int n, k;
cin >> n >> k;
for (int i = 0; i <= n; i++)
{
f[i] = i;
}
int ans = 0;
while (k--)
{
int d, x, y;
cin >> d >> x >> y;
if (x > n || y > n || (d == 2 && x == y))
{
ans++;
continue;
}
if (d == 1) // x,y是同类
{
if (findd(x) != findd(y)) // 如果x和y之间还没有任何关系,直接按照关系d合并
{
mergee(d, x, y);
}
else if (r[x] != r[y]) // 如果x和y之前已经存在某种关系,这里应该是r[x]+0!=r[y],对应上面的传递关系
{
ans++;
}
}
else // d==2 x吃y
{
if (findd(x) != findd(y))
{
mergee(d, x, y);
}
else if ((r[x] + 1) % 3 != r[y]) // 对应上述传递关系
{
ans++;
}
}
}
cout << ans << endl;
return 0;
}
solution2
三倍并查集方法。上面solution1的方法中结点
i
i
i 与父节点的关系有同类、吃与被吃三种,但是solution2的这个方法点之间的关系是通过不同群系之间的编号来确定的,边连接的两个点之间并没有某种确定的关系。
我们假设三个群系
G
0
G_0
G0、
G
1
G_1
G1、
G
2
G_2
G2,每个群系中都有相同的N个动物,分别编号1~n、n+1~2n、2n+1~3n。
对
G
i
G_i
Gi 来说,
G
(
i
−
1
+
3
)
%
3
G_{(i-1+3)\%3}
G(i−1+3)%3 狩猎
G
i
G_i
Gi,
G
(
i
+
1
)
%
3
G_{(i+1)\%3}
G(i+1)%3 被
G
i
G_i
Gi 狩猎,简单说就是
G
0
G_0
G0 吃
G
1
G_1
G1、
G
1
G_1
G1 吃
G
2
G_2
G2、
G
2
G_2
G2 吃
G
0
G_0
G0。
同类、吃、被吃这三种关系是通过不同群系之间的编号来确定的,只要它们合并到一个集合中,那么就可以通过编号来判断它们之间的关系。 点与点之间的连线并没有什么实际意义。
比如样例,上图是路径压缩后的并查集,
x
→
y
x\rightarrow y
x→y 表示
f
[
y
]
=
x
f[y]=x
f[y]=x,A吃B,B吃C,C吃A。
现在有三个集合 {A1,C3,B2},{B1,A3,C2},{C1,A2,B3}。比如第一个集合就可以通过A吃B、B吃C、C吃A来判断:A1吃B2,B2吃C3,C3吃A1。同理其他两个集合。
code2
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int f[N * 3]; // A:1~n B:n+1~2n C:2n+1~3n
int findd(int x)
{
return f[x] == x ? x : f[x] = findd(f[x]);
}
int main()
{
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
int n, k;
cin >> n >> k;
for (int i = 1; i <= n * 3; i++)
{
f[i] = i;
}
int ans = 0;
int t = 1;
while (k--)
{
int d, x, y;
cin >> d >> x >> y;
if (x > n || y > n || (d == 2 && x == y))
{
ans++;
continue;
}
if (d == 1)
{
// 判断x和y是否同类的方法是 x不吃y,y不吃x
if (findd(x) == findd(y + n) || findd(x + n) == findd(y))
{
ans++;
}
else
{
// 合并
f[findd(y)] = findd(x);
f[findd(y + n)] = findd(x + n);
f[findd(y + n + n)] = findd(x + n + n);
}
}
else
{
// 若x、y是同类,或者y吃x,则是假话
if (findd(x) == findd(y) || findd(x + n) == findd(y))
{
ans++;
}
else
{
// 合并
f[findd(y + n)] = findd(x);
f[findd(y + n + n)] = findd(x + n);
f[findd(y)] = findd(x + n + n);
}
}
}
cout << ans << endl;
return 0;
}