题目就不在这里贴出了。这题目我不会,虽然知道是一道并查集的题目。上网搜答案,乱看一气,有以下几点体会:
- 依然是并查集的find-union框架。
- 除父子关系信息(最基本的并查集)之外,还附加了“与根结点谁吃谁(或者同类)”的信息。
- find函数中,与以往靠while循环寻找始祖不同,这次是递归调用find函数寻找始祖—这导致了路径压缩的根本性改变:沿途的所有结点都直接指向始祖了!
规定
- 若有结点x,那么它的父亲结点是fx。
- 数组r[],它的下标是x,对应此下标储存的数据是x对fx的关系,记为x-->fx。具体而言,数组中储存的元素若是:0 x与fx是同类;1 x被fx吃;2 x吃fx。
至于为什么规定012而不是345,为什么规定0是同类1是被父亲吃2是吃父亲?原则上来讲可以任意规定,只要保证以后的推理都建立在这个规定的基础上即可。
但究竟是为什么?---因为网上都这么规定,那我们也这么规定吧(true story^_^)。事实上,这样的规定确有其方便之处,但只是聊胜于无罢了,这样的规定并非是必须的。
建立基本的关系递推公式
即:已知x-->y、y-->z,求x-->z。
这个表有什么用呢?---更新当前结点的父亲时用。具体而言,若儿子是x,则父亲是fx,爷爷是ffx;现在已知r[x](即x-->fx)和r[fx](即fx-->ffx),若压缩路径使得ffx是x的新父亲,那么新的r[x](即x-->ffx)应该是多少呢?
归纳表中数据,有(x-->ffx)=( (x-->fx) + (fx-->ffx) )%3,即r[x]=(r[x]+r[fx])%3 。这个关系递推表达式很重要,下面的分析就是建立在它的基础上。
递归形式的find函数
- //查找x的集合,回溯时压缩路径,并修改x与father[x]的关系
- int Find_set(int x)
- {
- int t;
- if(x!=father[x])
- {
- t = father[x];
- father[x]= Find_set(father[x]);//递归调用在此
- //更新x与father[x]的关系
- rank[x] = (rank[x]+rank[t])%3;
- }
- return father[x];
- }
这个递归形式的find函数有以下两个关键点:
- 回溯时,将沿途所有点的父亲更新成了它的始祖。从上图看出,f4=4、f3=4、f2=4、f1=4都是在回溯过程发生的。
- 由于在1中更新了沿途结点的父亲结点,于是上文的“关系递推公式”在此就派上了用场。
如上图,经过递归函数压缩路径,更新了父结点(1、2、3的父亲现在都是4了),但尚未更新r[1]、r[2]与r[3],那么这个更新过程是什么呢?
3-->4,4-->4,求3-->4。即r[3]=(r[3]+r[4])%3。
2-->3,3-->4(注意:上一句已经把3的父亲更新为4,虽然3的父亲本来就是4),求2-->4。即r[2]=(r[2]+r[3])%3。
1-->2,2-->4(注意:上一句已经把2的父亲更新为4),求1-->4。即r[1]=(r[1]+r[2])%3。
因此在回溯时,也更新了沿途所有结点与他们的新父亲(which is 原来的始祖)的关系。
如何union
有了以上基础,union函数理解起来就是水到渠成的事儿。
什么是union呢?在并查集数据结构中,把本应属于同一个集合,但是目前处在两个不同集合的结点树进行合并的过程就是“并”;那么如何知道两个“本来应该在一起”的结点一开始在不在一起呢?答案自然是“查”。
前文我们已经分析过“查”了,希望大家还记得两点:1.“查”之后,沿途所有结点都有了新的父结点---它们的始祖;2.“查”之后,沿途所有结点与它们共同的新的父结点(which is 它们的始祖)的关系更新完毕。
由此可见,在“查”之后,树的高度变为1,一切都变得简单了。所以“查”是整个问题的核心。
题目输入数据的格式是:d x y。d是操作类型,1表示x与y同类,即y-->x的值为0,2表示x吃y,即y-->x的值为1。总结起来就是,y-->x的值恰好是(d-1)。插一句题外话,还记得我曾经提到,本文开头的规定“并非必须”但又“聊胜于无”么?这个规定的方便之处就是你可以用(d-1)这么一个简单的式子描述输入x与输入y的关系,无他。若y-->x的"同类、被父吃、吃父"三种关系被规定成"345"而非"012",那么我们用(d+2)描述y-->x便是。
回归正题,输入d x y,我们先查,查完后树高变为1,如下图:
现在已知:x-->fx、y-->fy以及y-->x,要把fy作为子树与fx树“并”,更新fy的父结点为fx很简单,但怎样计算fy-->fx的值呢?
有了前文关系递推公式的基础,不难看出(fy-->fx)=( (fy-->y) + (y-->x) + (x-->fx) )%3,即r[fy]=( 3-r[y] + d-1 + r[x])%3。
等等,以上操作结束后,r[x]保持不变,r[fy]获得更新,但是本应该更新的r[y]却还是老样子(还是y-->fy,其实应该更新为y-->fx)啊?
事实上,在需要用到y的时候,都会先执行“查”的操作;而这一操作本身会实现将y指向fx,以及更新r[y]。
代码清单
以下代码是这个网页:POJ 1182 食物链【经典并查集应用】中的代码,看了那么多版本,这个最为简洁、清晰,注释也写得很清楚。
- #include<cstdio>
- const int maxn = 50000+10;
- int p[maxn]; //存父节点
- int r[maxn];//存与父节点的关系 0 同一类,1被父节点吃,2吃父节点
- void set(int n) //初始化
- {
- for(int x = 1; x <= n; x++)
- {
- p[x] = x; //开始自己是自己的父亲节点
- r[x] = 0;//开始自己就是自己的父亲,每一个点均独立
- }
- }
- int find(int x) //找父亲节点
- {
- if(x == p[x]) return x;
- int t = p[x];
- p[x] = find(p[x]);
- r[x] = (r[x]+r[t])%3; //回溯由子节点与父节点的关系和父节点与根节点的关系找子节点与根节点的关系
- return p[x];
- }
- void Union(int x, int y, int d)
- {
- int fx = find(x);
- int fy = find(y);
- p[fy] = fx; //合并树 注意:被 x 吃,所以以 x 的根为父
- r[fy] = (r[x]-r[y]+3+(d-1))%3; //对应更新与父节点的关系
- }
- int main()
- {
- int n, m;
- scanf("%d%d", &n, &m);
- set(n);
- int ans = 0;
- int d, x, y;
- while(m--)
- {
- scanf("%d%d%d", &d, &x, &y);
- if(x > n || y > n || (d == 2 && x == y)) ans++; //如果节点编号大于最大编号,或者自己吃自己,说谎
- else 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++; // 如果 x 没有吃 y (注意要对应Uinon(x, y)的情况,否则一路WA到死啊!!!)
- }
- else Union(x, y, d); //如果开始没有关系,则建立关系
- }
- printf("%d\n", ans);
- return 0;
- }
-