题目来源:AcWing 240. 食物链
一、题目描述
动物王国中有三类动物 A A A, B B B, C C C,这三类动物的食物链构成了有趣的环形。
A A A 吃 B B B, B B B 吃 C C C, C C C 吃 A A A。
现有 N N N 个动物,以 1 1 1∼ N N N 编号。
每个动物都是 A A A, B B B, C C C 中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N N N 个动物所构成的食物链关系进行描述:
第一种说法是 1 X Y
,表示
X
X
X 和
Y
Y
Y 是同类。
第二种说法是 2 X Y
,表示
X
X
X 吃
Y
Y
Y。
此人对 N N N 个动物,用上述两种说法,一句接一句地说出 K K K 句话,这 K K K 句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
- 当前的话与前面的某些真的话冲突,就是假话;
- 当前的话中 X X X 或 Y Y Y 比 N N N 大,就是假话;
- 当前的话表示 X X X 吃 X X X,就是假话。
你的任务是根据给定的 N N N 和 K K K 句话,输出假话的总数。
输入格式
第一行是两个整数
N
N
N 和
K
K
K,以一个空格分隔。
以下 K K K 行每行是三个正整数 D D D, X X X, Y Y Y,两数之间用一个空格隔开,其中 D D D 表示说法的种类。
若 D = 1 D=1 D=1,则表示 X X X 和 Y Y Y 是同类。
若 D = 2 D=2 D=2,则表示 X X X 吃 Y Y Y。
输出格式
只有一个整数,表示假话的数目。
数据范围
1
≤
N
≤
50000
,
1≤N≤50000,
1≤N≤50000,
0
≤
K
≤
100000
0≤K≤100000
0≤K≤100000
输入样例:
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
输出样例:
3
二、算法思路
概括:通过使用并查集维护每个结点与根结点之间的距离,来表达某结点与根结点之间的关系。
假设现在有
a
a
a 动物和
b
b
b 动物,不管
a
a
a 和
b
b
b 是同类还是吃与被吃的关系,只要是能确定
a
a
a 与
b
b
b 存在某种关系,那么就将
a
、
b
a、b
a、b 放在并查集中的一个集合里。
举例:现在知道
x
x
x 和
y
y
y的关系,又知道
y
y
y 和
z
z
z 的关系,那么我一定能推断出
x
x
x 与
z
z
z 之间的关系(因为
x
x
x、
y
y
y、
z
z
z 的关系是一个传递闭包)。现在我们只需要先想办法确定某一个结点
A
A
A 与根结点
B
B
B 的关系,再利用同样的方法确定某一个结点
C
C
C 与根结点
B
B
B 之间的关系,我们就可以通过闭环关系得到
A
A
A 与
C
C
C 之间的关系。
由于 A A A、 B B B、 C C C 之间的关系是相互循环被吃,那么我们就用每个结点到根结点之间的距离来表示它和根结点之间的关系。
下面定义如下距离关系:下一层的吃上一层
(1)如果某个结点到根结点之间的距离是1,表示该结点可以吃掉根结点。即 该结点 吃 根结点
。
(2)如果某个结点到根结点之间的距离是2,表示该结点可以吃掉其父结点,因为其父结点又可以吃掉根结点,因此由循环关系可得根结点一定可以吃掉距离为2的顶点。即根结点 吃 该结点
。
(3)如果某个结点距离根结点的距离为3,那么说明该结点与根结点是同类。即 该结点 同类于 根结点
。
因此,所有结点与根结点之间的关系都可以通过将该结点距离根结点之间的距离 mod 3所得的结果(0,1, 2)判断出当前结点与根结点之间的关系。
(1)
d
i
s
dis
dis(该结点) % 3 == 0 :该结点与根结点同类
(2)
d
i
s
dis
dis(该结点) % 3 == 1 :该结点吃根结点
(3)
d
i
s
dis
dis(该结点) % 3 == 2 :该结点被根结点吃
那么,在代码实现的时候,如何记录距离这个概念呢?
就像记录父节点一样,
d
i
s
[
i
]
dis[i]
dis[i] 可以用来只记录
i
i
i 结点到其父结点的距离。在做路径压缩的时候,将
d
i
s
t
[
i
]
dist[i]
dist[i] 不断加上路径上的距离,最终路径压缩完成时,
d
i
s
[
i
]
dis[i]
dis[i] 记录的就是
i
i
i 结点到根结点的距离。
例如:在对粉色结点进行find操作时,如下图。
如果现在遇到一个真话,并且真话中涉及的两个动物x和y还没有建立关系,该如何将他们所在的关系集合合并呢?
因为关系的集合的非常简单,只需要执行
p
[
p
x
]
=
p
y
p[px] = py
p[px]=py 即可,但是如何设定
d
[
p
x
]
d[px]
d[px] 的值,成为了维护
x
x
x 和
y
y
y 关系的关键。
(1)如果需要保证
x
x
x 和
y
y
y 同类:说明
(
d
[
x
]
+
d
[
p
x
]
)
%
3
=
d
[
y
]
%
3
(d[x] + d[px]) \% 3 = d[y] \% 3
(d[x]+d[px])%3=d[y]%3,即
(
d
[
x
]
+
d
[
p
x
]
−
d
[
y
]
)
%
3
=
0
(d[x] + d[px] - d[y]) \% 3 = 0
(d[x]+d[px]−d[y])%3=0,推导出
d
[
p
y
]
=
d
[
y
]
−
d
[
x
]
d[py] = d[y] - d[x]
d[py]=d[y]−d[x]。
(2)如果需要保证
x
x
x 吃
y
y
y 的关系:说明
(
d
[
x
]
+
d
[
p
x
]
)
%
3
−
d
[
y
]
%
3
=
1
(d[x] + d[px]) \% 3 - d[y] \% 3 = 1
(d[x]+d[px])%3−d[y]%3=1,即
(
d
[
x
]
+
d
[
p
x
]
−
d
[
y
]
−
1
)
%
3
=
0
(d[x] + d[px] - d[y] - 1) \% 3 = 0
(d[x]+d[px]−d[y]−1)%3=0,推导出
d
[
p
x
]
=
d
[
y
]
+
1
−
d
[
x
]
d[px] = d[y] + 1 - d[x]
d[px]=d[y]+1−d[x]。
三、代码
#include <iostream>
using namespace std;
const int N = 5e4 + 10;
// d[x]记录的是x到p[x]之间的距离
int p[N], d[N];
int n, m;
int find(int x)
{
if (x != p[x])
{
// 总思路:先更新距离,再更新父结点为根结点
// 经过find(p[x])之后d[p[x]]存储的是x的父结点到根结点的距离
// 同时t指的是根结点
// 这里没有让p[x]=find(p[x])的原因是还要加上x到原始p[x]的距离
int t = find(p[x]);
d[x] += d[p[x]];
p[x] = t;
}
return p[x];
}
int main()
{
cin >> n >> m;
// 初始化,d[i]默认为0
for (int i = 1; i <= n; i++) p[i] = i;
int res = 0; // 假话的个数
while (m--)
{
int t, x, y;
cin >> t >> x >> y;
if (x > n || y > n) res++;
else
{
int px = find(x), py = find(y);
// 假如x和y同类
if (t == 1)
{
// 假如当前已经建立了关系(已经在同一个集合中)
if (px == py && (d[x] - d[y]) % 3) res++;
// 说明还没有建立关系
else if (px != py)
{
// 如果说要将两个集合合并:p[px] = py,并且满足x与y同类,那么要人为定义d[px]的距离
// (d[x] + d[px] - d[y]) % 3 = 0 即 d[px] = d[y] - d[x]
p[px] = py;
d[px] = d[y] - d[x];
}
}
else
{
// 如果说要将两个集合合并:p[px] = py,并且满足x吃y,那么要人为定义d[px]的距离
// (d[x] + d[px] - d[y] - 1) % 3 = 0 即 d[px] = d[y] + 1 - d[x]
if (px == py && (d[x] - d[y] - 1) % 3) res++;
else if (px != py)
{
p[px] = py;
d[px] = d[y] + 1 - d[x];
}
}
}
}
cout << res << endl;
return 0;
}