poj1182:食物链
poj1182:食物链
听说是poj中最经典的一道并查集题目。我一做,果然很经典呢!好难啊!!!真的琢磨了很久还弄懂。这道题的重点就在于怎么用并查集表示题目中的关系环。
1. 题干
2. 思路详解
实际上,在做这道题之前,我对并查集的了解就只停留在权重选择和压缩路径上。也就是大家司空见惯的那种模板。顺带再默写一遍复习一下:
#include <vector>
using namespace std;
class unionfind {
public:
vector<int> id, rank;
void init(int n){
id.clear(); rank.clear();
id.resize(n + 1); id.assign(n + 1, 1);
for (int i = 0; i < n + 1; i++) {id[n] = n;}
}
int find(int i){
while (id[i] != i){
id[i] = id[id[i]];
i = id[i];
}
return i;
}
void Union(int id1, int id2){
id1 = find(id1); id2 = find(id2);
if (rank[id1] > rank[id2]) {id[id2] = id1; rank[id1] += rank[id2];}
else {id[id1] = id2; rank[id2] += rank[id1];}
}
}uf;
经典的权值+压缩路径对吧?这应对模板题就够用了,模板题一套就出来了。
但是这道题不可以用这个方向去思考。这道题的rank数组不能是权重,而应该是关系约束。
2.1 题意抽象
题目的意思很简单,现在有三个物种,如果分别命名为 A,B,C,那么他们之间关系则为:A吃B,B吃C,C吃A,然后判断一个人说的话一是数据是否合法,二是是否前后矛盾。
首先要明确的是,如何用并查集代表三个物种?我们事先并没有办法给他们下一个定义,自然没有办法直接的将它们划分成对应的ABC。所以直接划分成这条路走不通。
不过其实我们可以换一个角度,从另一个视角去分析怎么划分种类。这个视角有点像是物理里的相对运动。
我们把焦点放在某个物种上。假设我是其中一个生物,在我的视角看来,我和其他的所有生物的种类的关系应该是怎么样的呢?
思考一下,不难发现,对于我来说,我面前的种类应该划分成3部分:
- 和我是同类的
- 会来吃我的种类
- 我会去吃的种类
不管我是A,B还是C,我总能把所有我能看到的生物分成这三个部分,而我并不需要在意自己从属于哪个种类。也就是说,如果以我自己为中心进行观察,我并不需要关心自己或者别人到底是A,B还是C,我只需要根据关系就能把其他所有物种分成3个部分。
我们回到并查集,看看并查集的特点。并查集的每一个集合,是不是选一个点作为根,其他所有的点都指向这个根节点?
发现了并查集的集合和上面题意抽象的关系吗?我相当于集合里的根,其他所有的动物相当于点,都指向根,根只需要确定根和某个点关系,就能完整地把环状关系描述出来。
所以,这里的rank数组,指代的就不再是权重,而是子节点与父节点的关系。为了方便编程,我们规定,在rank中,0代表同类,1代表父节点捕食子节点,2代表子节点捕食父节点。
这么规定的好处是,如果 (rank1 + 1) % 3 == rank2
可以说明两者是捕食关系,利用了相邻的性质得出来的。
2.2 find函数的相关分析
find
函数我们当然就要考虑路径压缩。这里的路径压缩有不同的地方。因为rank数组不再代表为权重而是关系,在压缩路径的同时,子父节点的关系很有可能是会发生改变的。
我们函数的设计采用递归式设计。这样比较方便,可以只考虑子节点直接以爷爷节点为父节点时关系的变化。
我们先思考一下,子节点与爷爷节点要怎么确定?我们知道子节点与父节点的关系,知道父节点与爷爷节点的关系,是不是可以尝试用这个条件作跳板推导出子节点与爷爷节点之间的关系?
实际上呢就是可以的。我们直接将左右情况都找出来看看,实际上是有9种情况,很容易就找到了。
比如说,如果父节点与子节点的关系是同类,父节点与爷爷节点的关系是捕食,那么可以得出子节点与爷爷节点的关系是捕食。以此类推,可以推出以下表格。
0 | 1 | 2 | |
---|---|---|---|
0 | 0 | 1 | 2 |
1 | 1 | 2 | 0 |
2 | 2 | 0 | 1 |
注:列为 r1
,表示父节点与子节点的关系,行为 r2
,表示爷爷节点与父节点的关系。假设 r3
表示爷爷节点与子节点的关系。
如果我说根据表格的规律,可以直接推出来, $r_3 = (r_1 + r_2) mod 3 $ ,能接受吗?实际上就找个规律就出来了。
所以, find
函数就很好写了。在子节点挂到爷爷节点上之后,按上面那个公式更新一下关系数组。
2.3 union函数的相关分析
union
函数最难的地方还是在关系的更新啊~~
虽然我们可以通过 find
函数找到根节点,但是两个根节点合并的时候,我们其实是不能直接得到两个根节点的关系的,需要经过推导。自己可以举例试试,关系的更新还要推导一下的。
首先我们先分析一下,如果告诉我们X和Y的关系,他们俩关系数组要怎么更新。题目有提到,1为同类,2为A吃B,可以简单讨论一下。
当 type == 1
时,XY为同类,rank更新为 rx = ry = type - 1 = 0
.
当 type == 2
时,输入为 X Y,表示X吃Y,(如果输入反过来就是Y吃X),rank更新为 rx or ry = type - 1
-->
指的是前者挂在后者上。挂完之后子节点的rank
会有更新。
所以X-->Y
之间的关系表达式就是 type - 1
。反过来是 (4 - type)%3
反过来求这个操作实际上就是已知X和Y是捕食,那么Y和X是被捕食,然后用算式表达式表示一下。
我们接下来考虑X和fx(X的根节点)之间关系的转换。
X-->fx
因为 rx
存的本来就是对应的关系,所以表达式为 rx
。fx-->X
需要推导一下,得到 (3 - rx) % 3
按照同样的推导,可以得到 Y-->fy
fy-->Y
。分别是 ry
(3 - ry) % 3
。重点是 fy -- > fx
我们先把它假设成 rxy
吧!
这样的话,我们可以列出一个转换式子:fy --> Y --> X == fy --> X
重点是怎么化简。还记得 find
函数推出来的结论吗?子节点挂到爷爷节点的更新关系是 (r1 + r2) % 3
,所以 fy --> X
很自然就变成了 (d - 1 + 3 - ry) % 3
由于有fy --> X --> fx == fy --> fx
可以得到是 (d - 1 + 3 - ry + rx) % 3
,于是更新函数就这样搞定了……
2.3 转化成符合题意的代码
最首要的条件是数据是否合法,不合法直接算说谎。题目已经指出所有不合法的情况,直接判断即可。
接下来是在同一集合的情况。
- 如果输入为同类但是
rank
的值不同,那就是说谎。 - 如果输入为捕食,如果
(rank[x] + 1) % 3 != rank[y]
说明说谎
如果不在一个集合,说明之前没有定义过,将两个合并。
最后输出即可。
3. AC代码(C++)
#include <cstdio>
#include <vector>
using namespace std;
class uf{
public:
vector<int> id, rank;
void init(int n){
id.resize(n); rank.resize(n);
for (int i = 0; i < n; i++) {id[i] = i;}
}
int find(int i){
if (id[i] == i) return i;
int t = id[i];
id[i] = find(id[i]);
rank[i] = (rank[i] + rank[t]) % 3;
return id[i];
}
void Union(int x, int y, int type){
int fx = find(x), fy = find(y);
id[fy] = fx;
rank[fy] = (3 - rank[y] + type - 1 + rank[x]) % 3;
}
}uf;
int main(){
int num = 0, k = 0, cnt = 0;
int type = 0, id1 = 0, id2 = 0;
scanf("%d%d", &num, &k);
uf.init(num + 1);
for (int i = 0; i < k; i++){
scanf("%d%d%d", &type, &id1, &id2);
if (id1 > num || id2 > num || (id1 == id2 && type == 2)) {cnt++;}
else if (uf.find(id1) == uf.find(id2)){
if (type == 1 && uf.rank[id1] != uf.rank[id2]) {cnt++;}
if (type == 2 && (uf.rank[id1] + 1) % 3 != uf.rank[id2]) {cnt++;}
}
else {uf.Union(id1, id2, type);}
}
printf("%d\n", cnt);
return 0;
}