POJ 1182 : 食物链
由于题目中的N个动物都是A,B,C三类中的其中一种。题目有两种操作方式,【1】”1 X Y”,表示X和Y是同类,这个操作就是并查集中的【并】操作,【2】”2 X Y”,表示X吃Y,这个操作实际上是维护了森林中树之间的关系(类似有向图,此时树类比成一个顶点)。题目求这两个操作不成功的个数,假设为ans。
问题拆分
【1】进行1 X Y表示X和Y是同类之前,首先得判断X是否能吃Y或者Y是否能吃X。如果可以则证明操作不成功,ans累加1,否则将X和Y合并。
【2】2 X Y表示X吃Y时,首先得判断Y是否能吃X或者X和Y是否是同类。如果可以则证明操作不成功,ans累加1,否则建立X和Y的关系。
方法一
通过数组建立分类(A,B,C),首先1~N是A类,(N+1)~ 2 * N是B类,(2 * N + 1)~3 * N是C类。N是动物总数。
上面的判断可以分解为:
【1】X是否能吃Y:X在A类,Y在B类;X在B类,Y是C类;X在C类,Y在A类。
【2】Y是否能吃X:Y在B类,X在A类;Y在C类,X是B类;Y在A类,X在C类。
【3】X和Y是否是同类:X,Y在A类 ;X,Y在B类 ; X,Y在C类。
假设输入例子如下
100 8
2 1 2
2 2 3
1 1 4
1 2 5
1 3 6
1 3 9
2 3 2
1 1 2
1 1 3
执行过程演示图如下:
代码
#include <iostream>
#include <cstdio>
#include <map>
using namespace std;
const int MAXNUM = 3* (50000 + 10);
int id[MAXNUM];
int Size[MAXNUM];
// 初始化
void make_set(int n){
for(int i = 1 ; i <= n ; i++){
id[i] = i;
Size[i] = 1;
}
}
// 查找父节点
int Find(int p) {
while (p != id[p]) {
// 路径压缩,会破坏掉当前节点的父节点的尺寸信息,因为压缩后,当前节点的父节点已经变了
id[p] = id[id[p]];
p = id[p];
}
return p;
}
// 合并 p ,q节点
void Union(int p, int q) {
int pRoot = Find(p);
int qRoot = Find(q);
if (pRoot == qRoot) {
return;
}
// 按秩进行合并
if (Size[pRoot] > Size[qRoot]) {
id[qRoot] = pRoot;
Size[pRoot] += Size[qRoot];
} else {
id[pRoot] = qRoot;
Size[qRoot] += Size[pRoot];
}
}
//判断x和y是否属于同一个集合
bool same(int x,int y){
return Find(x) == Find(y);
}
int main(){
//freopen("input.txt","r",stdin);
//freopen("output.txt","w",stdout);
int N,K;
int t,x,y;
scanf("%d %d", &N,&K);
make_set(N * 3);
int ans = 0;
for(int i = 0;i < K; i++){
scanf("%d %d %d",&t,&x,&y);
if(x <= 0|| N < x || y <= 0 || N < y){
ans++;
continue;
}
//分析x和y是同类是否正确
if(t == 1){
//如果x吃y || y吃x就不正确
if(same(x, y + N) || same(x, y + 2 * N)){
ans++;
}else{
//x和y是同类 正确
Union(x, y); //如果x属于A,y就属于A
Union(x+N, y+N); //如果x属于B,y就属于B
Union(x+N*2, y+N*2); //如果x属于C,y就属于C
}
}else{
//分析x吃y是否正确
//如果x和y是同类 || y吃x就不正确
if(same(x, y) || same(x, y + 2 * N)){
ans++;
}else{
Union(x,y + N); //如果x属于A,那么y就属于B
Union(x + N,y + 2 * N); //如果x属于B,那么y就属于C
Union(x + 2 * N, y); //如果x属于C,那么y就属于A
}
}
}
printf("%d\n",ans);
return 0;
}
结合演示图和代码可知,
(1)合并操作将会在这三棵树中执行,最终维护的树主要是这三棵,剩余的都是结点树为1的树。
(2)same函数的比较,都是比较x和y + N,y + 2 * N的根节点是否相同。也就是说在same函数中在A类和B类,C类的根节点进行比较。
(3)在某棵树下的类型和在整个森林下的类型不一样,这个很关键,也是理解整个操作的关键。假设1号是A类,2号是B类,3号是C类,N=100,从而1吃2,2吃3,3吃1,这些在题意和例子中可以得到的假设。这三棵树从左到右编号分别是f1,f2,f3。
那么在f1中,1号是C类(因为201,204在2 * N + 1 ~ 3 * N之间),2号是A类(因为2,5在1 ~N之间),3号是B类(103,106,109在N + 1 ~ 2 * N之间)。
在f2中,1号是A类(因为1,4在1 ~N之间),2号是B类(因为102,105在N + 1 ~ 2 * N之间),3号是C类(因为203,206,209在2 * N + 1 ~ 3 * N之间)。
在f3中,1号是B类(因为101,104在N + 1 ~ 2 * N之间),2号是C类(因为202,205在2 * N + 1 ~ 3 * N之间),3号是A类(因为3,6,9在1 ~N之间)。
(4)由(3)可以得知(x, y + N)就是假设在某个树下,x是A类,y是B类,如果根节点相同则证明在同一颗树,同时证明含有x吃y的关系。而same(x, y + 2 *N)类似。
(5)演示图的结果不是按照秩去合并,只是为了更加直观看清楚效果,并不影响最终结果。
方法二:
参考:https://blog.csdn.net/freezhanacmore/article/details/8767413
把确定了相对关系的节点放在同一棵树中
每个节点对应的 r[]值记录他与根节点的关系:
0:同类,
1:被父亲节点吃,
2: 吃父亲节点
每次输入一组数据 d, x, y判断是否超过 N 后,先通过find()函数找他们的根节点从而判断他们是否在同一棵树中。(也就是是否有确定的关系)
1.如果在同一棵树中find(x) == find(y):直接判断是否说谎。
1)如果 d ==1,那么 x 与 y 应该是同类,他们的r[]应该相等
如果不相等,则说谎数 +1
2)如果 d==2,那么 x 应该吃了 y,也就是 (r[x]+1)%3 == r[y]
如果不满足,则说谎数 +1
如何判断 x 吃了 y 是 (r[x]+1)%3 == r[y],请看下图:(PS:箭头方向指向被吃方)
2.如果不在同一棵树中:那么合并 x 与 y 分别所在的树。
合并树时要注意顺序,我这里是把 x 树的根当做主根,否则会WA的很惨
注意:找父亲节点时,要不断更新 r[]的值。
这里有一个关系:如果 x 和y 为关系 r1, y 和 z 为关系 r2
那么 x 和z的关系就是 (r1+r2)%3
如何证明?
无非是3*3 = 9种情况而已
(a, b) 0:同类 、 1:a被b吃 、 2:a吃b
(x, y) | (y, z) | (x,z) | 如何判断 |
---|---|---|---|
0 | 0 | 0 | 0+0 = 0 |
0 | 1 | 1 | 0+1 = 1 |
0 | 2 | 2 | 0+2 = 2 |
1 | 0 | 1 | 1+0 = 1 |
1 | 1 | 2 | 1+1 = 2 |
1 | 2 | 0 | (1+2)% 3 = 0 |
2 | 0 | 2 | 2+0 = 2 |
2 | 1 | 0 | (2+1)% 3 = 0 |
2 | 2 | 1 | (2+2)% 3 = 1 |
关于合
并时r[]值的更新:
如果 d == 1则 x和y 是同类 ,那么 y 对 x 的关系是 0
如果 d == 2 则 x 吃了 y, 那么 y 对 x 的关系是 1, x 对 y 的关系是 2.
综上所述 ,无论 d为1 或者是为 2, y 对 x 的关系都是 d-1
定义 :fx 为 x 的根点, fy 为 y 的根节点
合并时,如果把 y 树合并到 x 树中
如何求 fy 对 fx 的r[]关系?
fy 对 y 的关系为 3-r[y]
y 对 x 的关系为 d-1
x 对 fx 的关系为 r[x]
所以 fy 对 fx 的关系是(3-r[y] + d-1 + r[x])%3
代码
#include <iostream>
#include <cstdio>
#include <map>
using namespace std;
const int MAXNUM = 50000 + 10;
int id[MAXNUM];
int Size[MAXNUM];
int r[MAXNUM];//存与父节点的关系 0 同一类,1被父节点吃,2吃父节点
// 初始化
void make_set(int n){
for(int i = 1 ; i <= n ; i++){
id[i] = i;
Size[i] = 1;
r[i] = 0;
}
}
// 查找父节点
int Find(int p) {
while (id[p] != id[id[p]]) { //如果q不是其所在子树的根节点的直接孩子
r[p] = (r[p] + r[id[p]] ) % 3;
id[p] = id[id[p]]; //对其父节点到其爷爷节点之间的路径进行压缩
}
return id[p];
}
// 合并 p ,q节点
void Union(int p, int q, int d) {
int pRoot = Find(p);
int qRoot = Find(q);
if (pRoot == qRoot) {
return;
}
id[qRoot] = pRoot;
r[qRoot] = (r[p] - r[q] + 3 + (d - 1)) % 3;
}
int main(){
//freopen("input.txt","r",stdin);
//freopen("output.txt","w",stdout);
int N,K;
int d,x,y;
scanf("%d %d", &N,&K);
make_set(N);
int ans = 0;
for(int i = 0;i < K; i++){
scanf("%d %d %d",&d,&x,&y);
if(x <= 0|| N < x || y <= 0 || N < y){
ans++;
continue;
}
if(Find(x) == Find(y)){
// 不是同类
if(d == 1 && r[x] != r[y])
ans++;
// 如果 x 没有吃 y
if(d == 2 && (r[x] + 1) % 3 != r[y])
ans++;
}else{
Union(x,y,d);
}
}
printf("%d\n",ans);
return 0;
}