并查集 
并查集,或称DSUDisjoint Set
Union  ,是一种管理元素所属集合的数据结构。每次查询时把沿途的节点的父节点一并设置成全局父节点,从而达到均摊复杂度
 
的超高效率(其中   为  
的反阿克曼函数,可以看作为常数复杂度)。基于并查集的许多特性(包括好写),人们开发出了许多不同种类不同用途的并查集,其中就包括带边权的权值并查集和对元素进行分类的种类并查集。这篇文章将探讨这两种并查集的实现及相关应用。
权值并查集 
在并查集的边上加上边权,每次查找父节点时把边权向上推送更新,从而达到类似于线段树
pushup 操作的效果。
UVA 11987 Almost Union-Find 
题目地址:UVA
11987 
题目难度:提高+/省选- 
有   个集合,  次操作。规定第   个集合里初始只有  。有三种操作:
输入两个元素   和  ,若   和  
不在一个集合中,合并两个元素的集合。 
输入两个元素   和  ,若   和   不在一个集合中,把   添加到   所在的集合。 
输入一个元素  ,查询   所在集合的元素个数和所有元素之和。 
 
输入格式: 
有几组数据。
每组数据第一行输入   和   两个整数。
每组数据以下   行,每行第一个数
  代表选择哪一个命令,若   是   或   命令,则再输入两个整数   和  。若   是  ,则输入一个整数  。
输入文件结束符(EOF)结束输入。
输出格式: 
输出行数为每组数据  
号命令的总数。
每一行输出两个整数   和  ,即元素个数和元素和。
数据范围: 
 , 。
原题面 PDF :Here 
 
这道题在普通并查集维护所属集合的要求外,另外要求我们维护并查集的集合大小及元素总和,因此考虑权值并查集。基本思路是:在每次合并操作时,更新被插入集合父节点的
sum 与 size,累加上待插入集合的
sum 与 size
即可。特殊一点,对于移动操作(相当于把待插入集合变成一个大小为   的单点)也是一样。
那么如何实现单点的移动呢?一般的思路是把点的父亲直接指向被插入集合的根,但这显然是错误的,因为:
当我们根据并查集的标准合并方式合并节点   到树   时,我们会把从属于   号的  
号节点也一并接过去,这是我们不希望的,期望效果是只把   送过去,只留下   号一个在原位置。
那我们就需要提出一种建图方式,使得   不直接与   相连,但是要求   又属于  
树。考虑为每个集合设置虚拟源点,初始时每个集合就是以虚拟源点为根,普通节点为叶子的两点集合。如此一来,移动操作就变成了下图:
然后就可以按照普通并查集的方法解了(因为创建了   个虚拟源点,空间要开二倍)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #include  <bits/stdc++.h>  #define  N 200010 using  namespace  std;typedef  long  long  ll;int  p[N], vol[N];ll sum[N]; int  find (int  x)   {    if  (p[x] != x) p[x] = find (p[x]);     return  p[x]; } void  merge (int  a, int  b)   {    int  u = find (a), v = find (b);     p[u] = v;     sum[v] += sum[u];     vol[v] += vol[u]; } void  move (int  a, int  dest)   {    int  u = find (a), v = find (dest);     p[a] = v;     vol[v]++;     vol[u]--;     sum[u] -= a;     sum[v] += a; } bool  related (int  a, int  b)   {    return  find (a) == find (b); } int  main ()   {    ios::sync_with_stdio (false );     cin.tie (nullptr );     cout.tie (nullptr );     int  n, m;     while  (cin >> n >> m) {         for  (int  i = 1 ; i <= 2  * n; i++) {             if  (i <= n) {                 p[i] = i + n;                 vol[i] = 1 ;                 sum[i] = i;             } else  if  (i > n) {                 p[i] = i;                 vol[i] = 1 ;                 sum[i] = i - n;             }         }         while  (m--) {             int  k, a, b;             cin >> k;             if  (k == 1 ) {                 cin >> a >> b;                 if  (!related (a, b)) merge (a, b);             } else  if  (k == 2 ) {                 cin >> a >> b;                 if  (!related (a, b)) move (a, b);             } else  {                 cin >> a;                 int  u = find (a);                 cout << vol[u] << ' '  << sum[u] << endl;             }         }     }     return  0 ; } 
 
练习:P1196
[NOI2002] 银河英雄传说 
种类并查集 
一般的并查集可以用来维护一个带有传递性和连通性的集合,例如你亲戚的亲戚仍然是你的亲戚,普通并查集就可以做到维护你的所有亲戚和非亲戚,并判断某人是否是你的亲戚;而种类并查集则是它的升级,普通的并查集显然无法轻易维护诸如“敌人的敌人是朋友”的人际关系。这时,我们就可以多开几个普通的并查集用来维护不同类别之间的关系,即种类并查集。
P2024 [NOI2001] 食物链 
题目地址:P2024 
题目难度:普及+/提高 
题目来源:NOI   2001 
动物王国中有三类动物  ,这三类动物的食物链构成了有趣的环形。  吃  , 
吃  ,  吃  。
现有   个动物,以   编号。每个动物都是  
中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这  
个动物所构成的食物链关系进行描述:
第一种说法是 1 X Y,表示   和   是同类。 
第二种说法是2 X Y,表示   吃  。 
 
此人对  
个动物,用上述两种说法,一句接一句地说出   句话,这  
句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
当前的话与前面的某些真的话冲突,就是假话; 
当前的话中   或   比   大,就是假话; 
当前的话表示   吃  ,就是假话。 
 
你的任务是根据给定的   和   句话,输出假话的总数。
输入格式: 
第一行两个整数, ,表示有
  个动物,  句话。
第二行开始每行一句话(按照题目要求,见样例)
输出格式: 
一行,一个整数,表示假话的总数。
数据范围: 
对于全部数据, , 。
 
这道题是种类并查集的经典应用之一,用来判断一个不等式组是否有冲突矛盾。之前在讲Floyd
传递闭包 的时候,我们就介绍了用传递闭包求解不等式组解的情况的方法。但是这种基于
 
算法的解法,其时间复杂度是恐怖的  ,在这道题里显然会超时,我们转而使用种类并查集来解决这道题。
整理一下题面,把这些动物分成三类——“国王”、“大臣”和“平民”。国王统治大臣、大臣压榨平民、平民推翻国王。但是关键是我们不知道动物的初始分类,因此我们在每一类里都放上
  个节点。
然后,对于操作  ——可能出现的矛盾是:二者不为同类,这里却归为同类;对于操作
 ,可能出现的是:倒反天罡(大臣搞国王、平民踩大臣)、自残(自己吃自己)。换句话说,如果某个节点的根指到其他区域,那么这两个动物在当前信息下不可能成为同类;如果不满足镇压关系的二者共用同一个根,显然也不成立。
根据并查集所带的实际意义判断即可。注意如果当前命题是正确的,不要忘记合并到并查集里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include  <bits/stdc++.h>  #define  N 50010 using  namespace  std;int  p[N * 3 ];int  n;int  find (int  x)   {    if  (p[x] != x) p[x] = find (p[x]);     return  p[x]; } void  merge (int  a, int  b)   {    p[find (a)] = find (b); } bool  related (int  a, int  b)   {    return  find (a) == find (b); } int  main ()   {    ios::sync_with_stdio (false );     cin.tie (nullptr );     cout.tie (nullptr );     int  k, ans = 0 ;     cin >> n >> k;     for  (int  i = 1 ; i <= 3  * n; i++) p[i] = i;     while  (k--) {         int  op, x, y;         cin >> op >> x >> y;         if  (x > n || y > n) ans++;         else  if  (op == 1 ) {             if  (related (x + n, y) || related (x, y + n)) ans++;             else  {                 merge (x, y);                 merge (x + n, y + n);                 merge (x + 2  * n, y + 2  * n);             }         } else  {             if  (related (x, y) || related (x, y + n)) ans++;             else  {                 merge (x, y + 2  * n);                 merge (x + n, y);                 merge (x + 2  * n, y + n);             }         }     }     cout << ans << endl;     return  0 ; } 
 
练习:P1955
[NOI2015] 程序自动分析