1 #include <cstdio> 2 #include <cstdlib> 3 #include <cstring> 4 #include <iostream> 5 //#define INPUT 6 /** 7 Problem:1182 - 食物链,NOI2001 8 Begin Time:4th/Mar/2012 1:00 p.m. 9 End Time:4th/Mar/2012 6:47 p.m. 10 Cost Time:两天多,看的别人的解题报告AC的 11 Reference:http://apps.hi.baidu.com/share/detail/16059767 12 测试数据: 13 http://poj.org/showmessage?message_id=93058 14 输出: 15 上方有 16 教训: 17 WA一次,没搞清楚先更新父节点relation还是更新当前节点relation的关系!!! 18 (在最后那条犯错误了!) 19 思路: 20 老子决心要写一个,关于这道题的,最详细的解题报告。 21 本题思路是带权并查集,我们从最开始讲起。 22 Part I - 权值(relation)的确定。 23 我们根据题意,森林中有3种动物。A吃B,B吃C,C吃A。 24 我们还要使用并查集,那么,我们就以动物之间的关系来作为并查集每个节点的 25 权值。 26 注意,我们不知道所给的动物(题目说了,输入只给编号)所属的种类。 27 所以,我们可以用动物之间“相对”的关系来确定一个并查集。 28 0 - 这个节点与它的父节点是同类 29 1 - 这个节点被它的父节点吃 30 2 - 这个节点吃它的父节点。 31 注意,这个0,1,2所代表的意义不是随便制定的,我们看题目中的要求。 32 说话的时候,第一个数字(下文中,设为d)指定了后面两种动物的关系: 33 1 - X与Y同类 34 2 - X吃Y 35 我们注意到,当 d = 1的时候,( d - 1 ) = 0,也就是我们制定的意义 36 当 d = 2的时候,( d - 1 ) = 1,代表Y被X吃,也是我们指定的意义。 37 所以,这个0,1,2不是随便选的 38 Part II - 路径压缩,以及节点间关系确定 39 确定了权值之后,我们要确定有关的操作。 40 我们把所有的动物全初始化。 41 struct Animal 42 { 43 int num; //该节点(node)的编号 44 int parent; //该node的父亲 45 int relation; //该node与父节点的关系,0同类,1被父节点吃,2吃父节点 46 }; Animal ani[50010]; 47 初始化为 48 For i = 0 to N do 49 ani[i].num = i; 50 ani[i].parent = i; 51 ani[i].relation = 0 ; //自己和自己是同类 52 End For 53 (1)路径压缩时的节点算法 54 我们设A,B,C动物集合如下:(为了以后便于举例) 55 A = { 1 , 2 , 3 ,4 ,5 } 56 B = { 6 , 7 , 8 ,9 ,10} 57 C = { 11, 12, 13,14,15} 58 假如我们已经有了一个集合,分别有3个元素 59 SET1 = {1,2},我们规定集合中第一个元素为并查集的“代表” 60 假如现在有语句: 61 2 2 6 62 这是一句真话 63 2是6的父亲 64 ani[6].parent = 2; 65 ani[6].relation = 1; 66 那么,6和1的关系如何呢? 67 ani[2].parent = 1; 68 ani[2].relation = 0; 69 我们可以发现6与2的关系是 1. 70 通过穷举我们可以发现 71 ani[now].parent = ani[ani[now].parent].parent; 72 ani[now].relation = ( ani[now].relation + ani[now.parent].relation ) % 3; 73 这个路径压缩算法是正确的 74 关于这个路径压缩算法,还有一点需要注意的地方,我们一会再谈 75 注意,根据当前节点的relation和当前节点父节点的relation推出 76 当前节点与其父节点的父节点的relation这个公式十分重要!! 77 它推不出来下面都理解不了!!自己用穷举法推一下: 78 好吧,为了方便伸手党,我给出穷举过程 79 i j 80 爷爷 父亲 儿子 儿子与爷爷 81 0 0 (i + j)%3 = 0 82 0 1 (i + j)%3 = 1 83 0 2 (i + j)%3 = 2 84 1 0 (i + j)%3 = 1 85 1 1 (i + j)%3 = 2 86 1 2 (i + j)%3 = 0 87 2 0 (i + j)%3 = 2 88 2 1 (i + j)%3 = 0 89 2 2 (i + j)%3 = 1 90 嗯,这样可以看到,( 儿子relation + 父亲relation ) % 3 = 儿子对爷爷的relation 91 这就是路径压缩的节点算法 92 (2) 集合间关系的确定 93 在初始化的时候,我们看到,每个集合都是一个元素,就是他本身。 94 这时候,每个集合都是自洽的(集合中每个元素都不违反题目的规定) 95 注意,我们使用并查集的目的就是尽量的把路径压缩,使之高度尽量矮 96 假设我们已经有一个集合 97 set1 = {1,2,7,10} 98 set2 = {11,4,8,13},每个编号所属的物种见上文 99 set3 = {12,5,4,9} 100 现在有一句话 101 2 13 2 102 这是一句真话,X = 13,Y = 2 103 我们要把这两个集合合并成一个集合。 104 直接 105 int a = findParent(ani[X]); 106 int b = findParent(ani[Y]); 107 ani[b].parent = a; 108 就是把Y所在集合的根节点的父亲设置成X所在集合的根节点。 109 但是,但是!!!! 110 Y所在集合的根结点与X所在集合的根节点的关系!!!要怎么确定呢? 111 我们设X,Y集合都是路径压缩过的,高度只有2层 112 我们先给出计算的公式 113 ani[b].relation = ( 3 - ani[Y].relation + ( d - 1 ) + ani[X].relation) % 3; 114 这个公式,是分三部分,这么推出来的 115 第一部分,好理解的一部分: 116 ( d - 1 ) :这是X和Y之间的relation,X是Y的父节点时,Y的relation就是这个 117 3 - ani[Y].relation = 根据Y与根节点的关系,逆推根节点与Y的关系 118 这部分也是穷举法推出来的,我们举例: 119 j 120 子 父相对于子的relation(即假如子是父的父节点,那么父的relation应该是什么,因为父现在是根节点,所以父.relation = 0,我们只能根据父的子节点反推子跟父节点的关系) 121 0 ( 3 - 0 ) % 3 = 0 122 1(父吃子) ( 3 - 1 ) % 3 = 2 //父吃子 123 2(子吃父) ( 3 - 2 ) % 3 = 1 //子吃父,一样的哦亲 124 —————————————————————————————————————————————————————— 125 我们的过程是这样的: 126 把ani[Y],先连接到ani[X]上,再把ani[Y]的根节点移动到ani[X]上,最后,把ani[Y]的根节点移动到ani[X]的根节点上,这样算relation的 127 还记得么,如果我们有一个集合,压缩路径的时候父子关系是这么确定的 128 ani[爷爷].relation = ( ani[父亲].relation + ani[儿子].relation ) % 3 129 我们已知道,( d - 1 )就是X与Y的relation了 130 而 (3 - ani[Y].relation)就是 以Y为根节点时,他的父亲的relation 131 那么 132 我们假设把Y接到X上,也就说,现在X是Y的父亲,Y原来的根节点现在是Y的儿子 133 Y的relation + ani[Y]根节点相对于ani[Y]的relation 134 ( ( d - 1 ) + ( 3 - ani[Y].relation) ) % 3 135 就是ani[Y]的父亲节点与ani[X]的relation了! 136 那么,不难得到,ani[Y]的根节点与ani[X]根节点的关系是: 137 ( ( d - 1 ) + ( 3 - ani[Y].relation) + ani[X].relation ) % 3 ->应用了同余定理 138 注意,这个当所有集合都是初始化状态的时候也适用哦 139 还是以最开头我们给的三个集合(分别代表三个物种)为例 140 2 1 6 141 带入公式 142 ani[6].relation = ( ( 2 - 1 ) + ( 3 - 0 ) + 0 ) % 3 = 1 143 也就是,6被1吃 144 Part III - 算法正确性的证明 145 首先,两个自洽的集合,合并以后仍然是自洽的 146 这个不难想吧,数学上有个什么对称性定理跟他很像的。 147 如果理解不了,就这么想!! 148 当set1和set2合并之后,set2的根节点得到了自己关于set1根节点的 149 正确relation值,变成了set1根节点的儿子,那么 150 set2的所有儿子只要用 151 ( ani[X].relation + ani[Y].relation ) % 3就能得到自己正确的relation值了 152 所以说,针对不在同一集合的两个元素的话,除非违背了(2)和(3),否则永远是真的 153 (无论这句话说的是什么,我们都可以根据所给X,Y推出两个子节点之间应有的关系,这个关系一确定,所有儿子的关系都可以确定) 154 其实所有的不同集合到最后都会被合并成一个集合的。 155 我们只要在一个集合中找那些假话就可以了。 156 首先,如何判断 157 1 X Y是不是假话。//此时 d = 1 158 if ( X 和 Y 不在同一集合) 159 Union(x,y,xroot,yroot,d) 160 else 161 if x.relation != y.relation ->假话 162 其次,如何判断 163 2 X Y是不是假话 //此时d = 2 164 if ( X 和 Y 不在同一集合) 165 Union(x,y,xroot,yroot,d) 166 else 167 (ani[y].relation + 3 - ani[x].relation ) % 3 != 1 ->假话 168 这个公式是这么来的: 169 3 - ani[x].relation得到了根节点关于x的relation 170 ani[y] + 3 - ani[x].relation得到了y关于x的relation 171 所以,只要y关于x的relation不是1,就是y不被x吃的话,这句话肯定是假话! 172 173 (2)路径压缩要特别注意的一点(错在这里,要检讨自己) 174 路径压缩的时候,记得要 175 先findParent,再给当前节点的relation赋值。 176 否则有可能因为当前节点的父节点的relation不正确而导致错的稀里哗啦。 177 例子: 178 set1 = {1,2,7,10} 179 set2 = {3,4,8,11} 180 set3 = {12,5,14,9} 181 Union(1,3,1,3,1) 182 Union(3,12,3,12,2) 183 1 5 1 184 算5的relation 185 如果不先更新parent的relation,算出来应该是 186 ( 3 - 0 + 0 + 1 ) % 3 = 1,5被1吃,显然不对 187 这里面,+ 0的那个0是指根节点 12 的relation(未更新,这里的0是指12与11的relation) 188 如果更新完了的话,应该是 189 ( 3 - 0 + 2 + 1 ) % 3 = 0 ,5与1是同一物种,对了 190 这里面的 2 是更新节点12的relation(12与1的relation) 191 后记: 192 关于这道题,我在网上搜索了许多解题报告,但是都闪烁其词,大概大家都不想 193 把自己辛辛苦苦推出来的公式写到网上供别人学习来节省时间吧。 194 我觉得这么做不好,对初学者容易产生不良影响,ACM如果只是一个小众化的圈子,那 195 岂不是太没意思了。 196 于是我就把我自己总结的这道题的经验放了出来,希望可以帮得到大家 197 自己总结的,对错也不知道,但是起码是“自洽”的,^ ^ 198 感谢那篇博文的博主,也感谢gzm,lqy两位学长的指导。 199 c0de4fun 200 */ 201 using namespace std; 202 const int c0de4fun = 50010;//动物个数的最大值 203 ///指明父节点与自己的关系,0同类,1被吃,2吃父 204 const int SAME = 0; 205 const int ENEMY = 1; 206 const int FOOD = 2; 207 struct Animal 208 { 209 int parent; 210 int num; 211 int relation; 212 }; 213 Animal ani[c0de4fun]; 214 long ans; 215 int findParent(Animal* node) 216 { 217 ///Wrong Answer 因为这个函数写错了 218 ///这个函数得是“自洽的” 219 ///就是说,得保证每个元素的父亲的relation是对的 220 ///再算自己的relation 221 ///因为自己的relation和父亲的relation有关 222 ///这就是为什么要先findParent再relation更新的原因 223 int tmp; 224 if( node->parent == node->num ) 225 return node->parent; 226 tmp = node->parent; 227 #ifdef DBG 228 printf("Animal %d s Parent is %d ",node->num,node->parent); 229 #endif 230 // node->relation = ( ani[node->parent].relation + node->relation ) % 3; 231 node->parent = findParent(&ani[node->parent]); 232 node->relation = ( ani[tmp].relation + node->relation ) % 3; 233 return node->parent; 234 } 235 void Union(int x,int y,int a,int b,int d) 236 { 237 ani[b].parent = a; 238 ///rootY.parent = rootX.parent; 239 ani[b].relation =( (3 - ani[y].relation) + (d - 1) + ani[x].relation) % 3; 240 } 241 242 void init_Animal(int n) 243 { 244 for(int i = 1 ; i <= n ; i++) 245 { 246 ani[i].num = i; 247 ani[i].parent = i; 248 ani[i].relation = SAME; 249 } 250 } 251 int main(int argc,char* argv[]) 252 { 253 int N,K; 254 int d,X,Y; 255 #ifdef INPUT 256 freopen("b:\acm\poj1182\input.txt","r",stdin); 257 #endif 258 scanf("%d%d",&N,&K); 259 init_Animal(N); 260 for(int i = 0 ; i < K ; i++) 261 { 262 scanf("%d%d%d",&d,&X,&Y); 263 if( X > N || Y > N) 264 ans++; 265 else 266 { 267 if(d == 2 && X == Y) 268 ans++; 269 else 270 { 271 int a = findParent(&ani[X]); 272 int b = findParent(&ani[Y]); 273 if ( a != b ) 274 { 275 ///x,y不在同一集合中 276 Union(X,Y,a,b,d); 277 } 278 else 279 { 280 switch(d) 281 { 282 case 1: 283 if(ani[X].relation != ani[Y].relation) 284 ans++; 285 break; 286 case 2: 287 if(((ani[Y].relation + 3 - ani[X].relation) % 3 ) != 1) 288 ans++; 289 break; 290 } 291 } 292 } 293 } 294 } 295 printf("%d ",ans); 296 return 0; 297 }