并查集
一.并查集的两个功能
1.查询一个集合中的一个元素(或者说查询某个元素在哪个集合中)
2.合并两个集合
二.优化
1.按集合的思想来看,并查集其实不是一个树形结构,就是一个普通的集合,查询是O(1),合并是O(n),必须扫描所有元素;
树形结构是优化出来的,下面的内容都是树形结构的,集合的那种就不说了,因为我们平时看到的并查集都是树形结构的
2.优化主要是两个,一个是查询的优化——压缩路径,一边查询一边修改,这个优化足够强大了。
压缩路径:原本的树形可能是无规则的,最坏的条件是一条链状的,查询时就是O(n),压缩路径的原理很简单,当某一次查询一个元素x的时候,从x回到祖先的路径中所有元素都做 修改,把他们的双亲p[i]都修改为祖先,也就是直接和祖先相连,那么修改后的结果就是
一种树形结构,树根(祖先)单独在一层,它的所有孩子都在同一层,即直接与树根相连,整棵树的层数是2,那么查询任何一个顶点的祖先时时间复杂度就是0(1)
在给出优化之前先给出两种查询代码,一种是递归式,一种是迭代式,路径压缩两种都可以
//递归式 int find(int x) { return p[x]==x ? x : p[x]=find(p[x]); } //迭代式(前部分是查询,后部分是路径压缩) int find(int x) //迭代式 { int i=x,r=x,j; while(r!=p[r]) //查询 r=p[r]; while(i!=r) //路径压缩 { j=p[i]; //先记录下i的双亲 p[i]=r; //修改i的直接与祖先相连 i=j; //接下来修改原本的双亲 } return r; //r就是祖先 }
代码的话很容易懂,没什么好说的,注释都写了
3.合并优化(启发式)
合并的话,知道两个集合的祖先a和b,也就是树根a和b,然后以a作为b的孩子连接上b上,或者b作为a的孩子连接在a上,这个是普通的
这个优化是基于树的结构,基本上很多树的合并都可以用这种思想。现在有两个集合需要合并,那么我们要知道两课树的高度,然后将高度小的合并在高度大的上面,为什么呢?因为这样子合并后的树的高度还是不变,还是原来比较高的那颗树的高度;如果两颗树的高度相同,那么哪个连在那个的上面都可以,但是注意一点,连接后,树的高度加1,为什么呢?很简单啊,两棵树高度相同,一个树根作为另一个树根的孩子连接上去了,那不就是新树增加了一层
merge3(a,b) { if (height(a) == height(b)) { height(a) = height(a) + 1; set[b] = a; } else if (height(a) < height(b)) set[a] = b; else set[b] = a; }
三.并查集的一些常用技巧
1.并查集判断图连通,或者找出图有多少个连通分量
并查集初始化话for(int i=1; i<=n; i++) p[i]=i;
结束后,扫描一遍p数组,看有多少个p[i]=i,其实p[i]=i,这个i就是某个连通分量的祖先,如果有超过一个祖先,那么就是有超过一个连通分量,就是图不连通,所以要找多少个连通分量的话也就是找祖先个数
2.判断某个连通分量是否成环
给你一条边(u,v),找到他们的祖先x,y,如果x和y相同,那么他们成环了,因为他们有共同祖先,也就是都有路径到这个共同祖先,而且他们两者本身也有边相连,成环。所以最小生成树kruskal算法中,就是利用这个性质来判断安全边的,凡是能成环的边都是非安全边,都舍去
3.统计一个连通分量中有多少个元素,需要再加一个数组c
初始化for(int i=1; i<=n; i++) c[i]=0;
然后并查集,结束后扫描整个顶点数组,找到每个顶点的祖先x,那么c[x]++;
当然这样比较慢,我们可以一边合并一边统计
初始化for(int i=1; i<=n; i++) c[i]=1; 一开始每个顶点都是独立,都是一个集合,集合的元素个数都是1
然后每次合并两个集合a,b,若是将b并到了a中,那么c[a]=c[a]+c[b],即每次合并都马上更新新的集合中的元素个数
注意c[i]的意思是 并不是说i所处的集合的元素个数 , 而是说,当i是一个集合的祖先(树根)时,它能代表这个集合的元素个数,那么c[i]才是这个集合的元素个数。所以说集合{1,5,8,10,12},假如说1才是树根,那么c[1]=5,其余的c[5],c[8],c[10],c[12]并不是