并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。
并查集(Disjoint-set data structure ---from wiki )的题目大体分为三个:普通的并查集,带种类的并查集,扩展的并查集(主要是必须指定合并时的父子关系,或者统计一些数据,比如此集合内的元素数目。)
♦普通并查集
比较简单、直接的不相交集合的合并和查询问题。此类并查集一般表示:如果a、b同集则表示a、b同类;否则a、b不同类。
模板:
1 //普通并查集,加上了路径压缩和Rank合并的优化 2 3 const int N=100005; 4 5 struct set 6 { 7 int parent; //记录父节点 8 int rank; //记录集合的节点数 9 }elem[N]; 10 11 int MAX; //最大集的元素个数 12 13 void init() 14 { 15 int i; 16 for(i=0;i<=N;i++) 17 { 18 elem[i].parent=i; 19 elem[i].rank=1; 20 } 21 } 22 23 int Find(int x) 24 { 25 if (elem[x].parent != x) //路径压缩 26 { 27 elem[x].parent = Find(elem[x].parent); 28 } 29 return elem[x].parent; 30 } 31 32 void Union(int a,int b) //合并两个集合 33 { 34 int x,y; 35 x=Find(a); 36 y=Find(b); 37 if(elem[x].rank>=elem[y].rank) 38 { 39 elem[y].parent=elem[x].parent; 40 elem[x].rank+=elem[y].rank; 41 if(MAX<elem[x].rank) 42 MAX=elem[x].rank; 43 } 44 else 45 { 46 elem[x].parent=elem[y].parent; 47 elem[y].rank+=elem[x].rank; 48 if(MAX<elem[y].rank) 49 MAX=elem[y].rank; 50 } 51 }
♠POJ 2524 Ubiquitous Religions (并查集入门 || 第一个并查集程序)
直接套模板即可。。。
1 #include <fstream> 2 #include <iostream> 3 #include <cstdio> 4 #include <cstdlib> 5 #include <cmath> 6 #include <iomanip> 7 #include <iomanip> 8 #include <climits> 9 #include <vector> 10 #include <stack> 11 #include <queue> 12 #include <list> 13 #include <set> 14 #include <map> 15 #include <algorithm> 16 #include <string> 17 #include <cstring> 18 19 using namespace std; 20 21 #define N 50005 22 23 //并查集 24 struct set 25 { 26 int parent; //记录父节点 27 int rank; //记录集合的节点数 28 }elem[N]; 29 30 int MAX; 31 int vis[N]; 32 33 void init() 34 { 35 int i; 36 for(i=0;i<=N;i++) 37 { 38 elem[i].parent=i; 39 elem[i].rank=1; 40 } 41 } 42 43 int Find(int x) 44 { 45 int root,temp; 46 temp=x; 47 while(x!=elem[x].parent) //寻找根节点 48 x=elem[x].parent; 49 root=x; 50 x=temp; 51 while (x!=elem[x].parent) //压缩路径,全部赋值为根节点的值 52 { 53 temp=elem[x].parent; 54 elem[x].parent=root; 55 x=temp; 56 } 57 return root; 58 } 59 60 void Union(int a,int b) //合并两个集合 61 { 62 int x,y; 63 x=Find(a); 64 y=Find(b); 65 if(elem[x].rank>=elem[y].rank) 66 { 67 elem[y].parent=elem[x].parent; 68 elem[x].rank+=elem[y].rank; 69 if(MAX<elem[x].rank) 70 MAX=elem[x].rank; 71 } 72 else 73 { 74 elem[x].parent=elem[y].parent; 75 elem[y].rank+=elem[x].rank; 76 if(MAX<elem[y].rank) 77 MAX=elem[y].rank; 78 } 79 } 80 81 int main() 82 { 83 int n,m; 84 int tt=0; 85 while(cin>>n>>m) 86 { 87 tt++; 88 int ans=0; 89 memset(vis,0,sizeof(vis)); 90 if (!n && !m) 91 return 0; 92 init(); 93 for (int i=0;i<m;i++) 94 { 95 int a,b; 96 cin>>a>>b; 97 Union(a,b); 98 } 99 for (int i=1;i<=n;i++) 100 if (!vis[Find(i)]) 101 { 102 ans++; 103 vis[Find(i)]=1; 104 } 105 cout<<"Case "<<tt<<": "<<ans<<endl; 106 } 107 108 return 0; 109 }
♦种类并查集
较复杂的并查集题目,此类并查集表示的已经不是同类、不同类的问题,而是a、b同集则表示a、b有关系;不同集表示a、b没关系。而同集有关系中又包含不同的种类问题。比如亲戚和非亲戚,而亲戚中有包含父亲、儿子等等关系,所以在同意并查集中处理不同种类比较麻烦。
关键词:标记、向量思维。
模板:
1 //种类并查集 ,Rank合并在这里就不用了,方便向量的汇总。 2 3 const int N=100005; 4 5 struct set 6 { 7 int parent; //记录父节点 8 int rank; //记录集合的节点数 9 int relation; 10 }elem[N]; 11 12 int MAX; //最大集的元素个数 13 14 void init() 15 { 16 int i; 17 for(i=0;i<=N;i++) 18 { 19 elem[i].parent=i; 20 elem[i].rank=1; 21 elem[i].relation=0; 22 } 23 } 24 25 int Find(int x) 26 { 27 int temp; 28 if (elem[x].parent != x) //路径压缩 29 { 30 temp=elem[x].parent; 31 elem[x].parent = Find(elem[x].parent); 32 elem[x].relation=(elem[temp].relation + elem[x].relation)%3; 33 } 34 return elem[x].parent; 35 } 36 37 void Union(int d,int a,int b) //合并两个集合 38 { 39 int x,y; 40 x=Find(a); 41 y=Find(b); 42 elem[x].parent=y; 43 elem[x].relation=(elem[b].relation-elem[a].relation+2+d)%3; 44 }
♠POJ 1182 食物链 (经典种类并查集)
思路:
题目告诉有3种动物,互相吃与被吃,现在告诉你m句话,其中有真有假,叫你判断假的个数(如果前面没有与当前话冲突的,即认为其为真话) 这题有几种做法,我以前的做法是每个集合(或者称为子树,说集合的编号相当于子树的根结点,一个概念)中的元素都各自分为A, B, C三类,在合并时更改根结点的种类,其他点相应更改偏移量。但这种方法公式很难推,特别是偏移量很容易计算错误。 下面来介绍一种通用且易于理解的方法: 首先,集合里的每个点我们都记录它与它这个集合(或者称为子树)的根结点的相对关系relation。0表示它与根结点为同类,1表示它吃根结点,2表示它被根结点吃。 那么判断两个点a, b的关系,我们令p = Find(a), q = Find(b),即p, q分别为a, b子树的根结点。 1. 如果p != q,说明a, b暂时没有关系,那么关于他们的判断都是正确的,然后合并这两个子树。这里是关键,如何合并两个子树使得合并后的新树能保证正确呢?这里我们规定只能p合并到q(刚才说过了,启发式合并的优化效果并不那么明显,如果我们用启发式合并,就要推出两个式子,而这个推式子是件比较累的活…所以一般我们都规定一个子树合到另一个子树)。那么合并后,p的relation肯定要改变,那么改成多少呢?这里的方法就是找规律,列出部分可能的情况,就差不多能推出式子了(对于任给的一个模型,如何快速推出式子?看一看这个博客里另一篇向量的思维模式吧~~~)。这里式子为 : tree[p].relation = (tree[b].relation – tree[a].relation + 2 + d) % 3; 这里的d为判断语句中a, b的关系。还有个问题,我们是否需要遍历整个a子树并更新每个结点的状态呢?答案是不需要的,因为我们可以在Find()函数稍微修改,即结点x继承它的父亲(注意是前父亲,因为路径压缩后父亲就会改变),即它会继承到p结点的改变,所以我们不需要每个都遍历过去更新。 2. 如果p = q,说明a, b之前已经有关系了。那么我们就判断语句是否是对的,同样找规律推出式子。即if ( (tree[b].relation + d + 2) % 3 != tree[a].relation ), 那么这句话就是错误的。 3. 再对Find()函数进行些修改,即在路径压缩前纪录前父亲是谁,然后路径压缩后,更新该点的状态(通过继承前父亲的状态,这时候前父亲的状态是已经更新的)。 核心的两个函数为: int Find(int x) { int temp_p; if (tree[x].parent != x) { // 因为路径压缩,该结点的与根结点的关系要更新(因为前面合并时可能还没来得及更新). temp_p = tree[x].parent; tree[x].parent = Find(tree[x].parent); // x与根结点的关系更新(因为根结点变了),此时的temp_p为它原来子树的根结点. tree[x].relation = (tree[x].relation + tree[temp_p].relation) % 3; } return tree[x].parent; } void Merge(int a, int b, int p, int q, int d) { // 公式是找规律推出来的. tree[p].parent = q; // 这里的下标相同,都是tree[p]. tree[p].relation = (tree[b].relation – tree[a].relation + 2 + d) % 3; } 而这种纪录与根结点关系的方法,适用于几乎所有的并查集判断关系(至少我现在没遇到过不适用的情况…可能是自己做的还太少了…),所以向大家强烈推荐~~ 搞定了食物链这题,基本POJ上大部分基础并查集题目就可以顺秒了,这里仅列个题目编号: POJ 1308 1611 1703 1988 2236 2492 2524。
有两种方法: 一:用的3倍数组,1~n是自己,n+1~2n是吃域,2n+1~3n是被吃域。然后x,y同类就x,y并起来,x+n,y+n并,x+2n,y+2n并,否则就和对方的吃域被吃域乱七八糟的并一并,然后Find时就找是在吃域里还是被吃域里。。。。。。 二:带相对偏移量的并查集: 用一个并查集表示两个元素有没有关系,然后在并查集里设置一个附属的相对偏移量,0表示和根节点同类,1表示吃根节点,2表示被根节点吃。 向量的思维模式: > 什么叫做向量的思维模式? > Orz Orz 我的理解是,对于集合里的任意两个元素a,b而言,它们之间必定存在着某种联系,因为并查集中的元素均是有联系的,否则也不会被合并到当前集合中。那么我们就把这2个元素之间的关系量转化为一个偏移量,以食物链的关系而言,不妨假设 a->b 偏移量0时 a和b同类 a->b 偏移量1时 a吃b a->b 偏移量2时 a被b吃,也就是b吃a 有了这些基础,我们就可以在并查集中完成任意两个元素之间的关系转换了。 不妨继续假设,a的当前集合根节点aa,b的当前集合根节点bb,a->b的偏移值为d-1(题中给出的询问已知条件) (1)如果aa和bb不相同,那么我们把bb合并到aa上,并且更新delta[bb]值(delta[i]表示i的当前集合根节点到i的偏移量) 此时 aa->bb = aa->a + a->b + b->bb,可能这一步就是所谓向量思维模式吧 上式进一步转化为:aa->bb = (delta[a]+d-1+3-delta[b])%3 = delta[bb],(模3是保证偏移量取值始终在[0,2]间) (2)如果aa和bb相同,那么我们就验证a->b之间的偏移量是否与题中给出的d-1一致 此时 a->b = a->aa + aa->b = a->aa + bb->b, 上式进一步转化为:a->b = (3-delta[a]+delta[b])%3, 若一致则为真,否则为假。
--------------------------------------------------------------------------------------------------------------------------------------------------------
1 #include <iostream> 2 #include <cstdio> 3 #include <cstdlib> 4 #include <cmath> 5 #include <iomanip> 6 #include <climits> 7 #include <vector> 8 #include <stack> 9 #include <queue> 10 #include <set> 11 #include <map> 12 #include <algorithm> 13 #include <string> 14 #include <cstring> 15 16 using namespace std; 17 18 typedef long long ll; 19 const double EPS = 1e-11; 20 21 void Swap(int &a,int &b){ int t=a;a=b;b=t; } 22 int Max(int a,int b) { return a>b?a:b; } 23 int Min(int a,int b) { return a<b?a:b; } 24 25 const int N=100005; 26 27 struct set 28 { 29 int parent; //记录父节点 30 int rank; //记录集合的节点数 31 int relation; 32 }elem[N]; 33 34 int MAX; //最大集的元素个数 35 36 void init() 37 { 38 int i; 39 for(i=0;i<=N;i++) 40 { 41 elem[i].parent=i; 42 elem[i].rank=1; 43 elem[i].relation=0; 44 } 45 } 46 47 int Find(int x) 48 { 49 int temp; 50 if (elem[x].parent != x) //路径压缩 51 { 52 temp=elem[x].parent; 53 elem[x].parent = Find(elem[x].parent); 54 elem[x].relation=(elem[temp].relation + elem[x].relation)%3; 55 } 56 return elem[x].parent; 57 } 58 59 void Union(int d,int a,int b) //合并两个集合 60 { 61 int x,y; 62 x=Find(a); 63 y=Find(b); 64 elem[x].parent=y; 65 elem[x].relation=(elem[b].relation-elem[a].relation+2+d)%3; 66 } 67 68 int main() 69 { 70 int n,k; 71 scanf("%d%d",&n,&k); 72 init(); 73 int num=0; 74 for (int i=0;i<k;i++) 75 { 76 int d,a,b; 77 scanf("%d%d%d",&d,&a,&b); 78 if (d==2 && a==b) 79 { 80 num++; 81 continue; 82 } 83 if (a>n ||b>n) 84 { 85 num++; 86 continue; 87 } 88 if (Find(a)!=Find(b)) 89 { 90 Union(d,a,b); 91 } 92 else 93 { 94 if ((elem[b].relation+d+2)%3 != elem[a].relation ) 95 num++; 96 } 97 98 } 99 printf("%d\n",num); 100 return 0; 101 }
♠POJ 1703 Find them, Catch them
思路:
和食物链那道题很像。只不过这里只有两个相对种类偏移量:0表示和根节点同帮派,1表示和根节点异帮派。
然后根据向量思维退出并查集中改变关系的式子:
1. aa->bb=aa->a+a->b+b->bb
在Union中:elem[y].relation=(elem[a].relation+1+elem[b].relation)%2;
2. aa->b=aa->bb+bb->b
在Find路径压缩时:elem[x].relation=(elem[elem[x].parent].relation+elem[x].relation)%2;
3.同理,判断时:1 or 0==a->b=a->aa+aa->b
--------------------------------------------------------------------------------------------------------------------------------------------------------
♦拓展并查集
(未完待续。。。)
其他待做并查集题目:
POJ-1308
用并查集来判断一棵树。。注意空树也是树,死人也是人。
POJ-1611
裸地水并查集
POJ-1988
看上去似乎和种类并查集无关,但其实仔细想想,就是种类并查集。。。
只不过是种类数目无穷大,通过合并,可以确定两个物品之间的种类差(即高度差)
POJ-2236
裸地并查集,小加一点计算几何
POJ-2492
裸地种类并查集
POJ-1456
常规思想是贪心+堆优化,用并查集确实很奇妙。。。下面的文章中有详细介绍。
POJ-1733
种类并查集,先要离散化一下,不影响结果。。。
HDU-3038
上一道题的扩展,也是种类并查集,种类无穷大。。。。
POJ-1417
种类并查集,然后需要背包原理来判断是否能唯一确定“好人”那一堆
POJ-2912
ZOJ-3261
逆向使用并查集就可以了。。。
POJ-1861 POJ-2560
Kruskal并查集