浅谈并查集优化

maniac!

2018-10-08 17:50:26

Personal

并查集是一种可以动态维护若干个不重叠的集合,并资瓷合并与查询的数据结构(~~这么水不配叫数据结构~~) (接下来的内容过于菜,请大佬出门左转) 并查集最基本的操作是查询与合并,为了提高效率,我们常用的我们引入了路径压缩与按秩合并两种思想 ## 路径压缩(最常用): ``` int get(int x){ if(x==f[x])return x; f[x]=get(f[x]); return f[x]; } void unionn(int x,int y){ int u=get(x),v=get(y); if(u!=v)f[u]=v; } ``` 给大家放张图理解下路径压缩的工作原理: ![路径压缩](https://cdn.luogu.com.cn/upload/pic/42259.png) 路径压缩的效率很高,可以做到$log$级别,可以防止树的形态变为一条链,在一般并查集中应用很广泛,一旦使用路径压缩,原有的父子关系就会被破坏,从而直接建立与祖先的父子关系,可以高效地维护集合之间的从属关系,但是对于每个节点之间的直接关系是不能维护的 路径压缩这里不再过多解释~~毕竟这么简单~~,我们来简述一下按秩合并,按秩合并分为按大小合并和按深度合并两种,按大小合并是很容易想到也很容易维护的,而按深度合并则是要将整个体系的从属关系抽象成一棵树,每次将深度小的合并到深度较大的树上,这样的话不论深度小的那棵树有多深,合并后的树的深度都不会大于一开始时的较大深度。 ## 按秩合并: ``` int get(int x){ while(f[x])x=f[x]; return x; } void unionn(int x,int y){//按大小合并 int u=get(x),v=get(y); if(u!=v){ if(size[u]<size[v]){ f[u]=v; size[v]+=size[u]; //按大小合并每次要更新大小 } else{ f[v]=u; size[u]+=size[v]; } } } ``` ``` void unionn(int x,int y){//按深度合并 int u=get(x),v=get(y); if(u!=v){ if(high[u]<hige[v])f[u]=v; else { f[v]=u; if(high[u]==high[v])high[u]++; //按深度合并只有在两树高度相等的时候更新 } } } ``` 而按秩合并与树的形态关系很密切,虽然不能像路径压缩一样快速处理两点是否有从属关系,它能很好地维护树的形态与父子关系,可以维护出每个子节点的直接父亲,这是路径压缩所不能做到的,由此来看,两种优化方式可以说是各有千秋。 这两种方法多数情况下是可以一起用的,既用路径压缩,又用按秩合并当然可以提高效率,复杂度增长速度为为阿克曼函数的反函数,不过只用路径压缩达到的效果($log$)在多数情况下也可以与之媲美,当然啦,依照个人习惯而定,愿意两个都用的当然可以,不过有时候按秩合并是不能与路径压缩一起使用的。仔细想一想我们刚才说到的性质,如果题目需要维护明确的父子关系而用到了按秩合并的话,是不能用路径压缩的,像上边所说,一旦用了路径压缩,会破坏树的形态,原来的节点会直接压缩到祖先上,这样一来我们调用的时候父子关系发生了改变,造成了算法的错误。 那么基础就讲到这里,下面看一下经典的扩展域和边带权问题 ## 扩展域 首先让我们从一道题目入手: 放一道老题 [食物链](https://www.luogu.org/problemnew/show/P2024) 因为题目告诉我们每三种动物构成一条食物链,我们可以将每种动物分成三部分,即同类$self$、捕食$eat$、天敌$enemy$,,那我们不妨将并查集数组开大三倍,作为并查集的扩展域。 即本身对应第一倍,猎物对应第二倍,天敌对应第三倍 嗯,举个栗子 如果是同类,就合并他们本身,他们的敌人,他们的猎物 ``` unionn(x,y); unionn(x+n,y+n) unionn(x+2*n,y+2*n); //(n为一倍大小) ``` 如果$x$吃$y$,说明$x$是$y$的天敌,那$x$的天敌就是$y$捕食的物种,也就是$x$吃$y$,$y$吃$z$,$z$吃$x$ ``` unionn(x+n,y); unionn(x,y+2*n); unionn(x+2*n,y+n); ``` 每次先判断是不是假话,也就是看一下是否已经被合并过,并且之前合并的关系与当前关系是否冲突,然后就可以按照题目所给出的关系进行合并啦~ 在做这道题之前不妨先做一下这道题 [团伙](https://www.luogu.org/problemnew/show/P1892) 食物链是这道题运用的反集思想的扩展,(食物链用的是三倍空间,团伙用的是二倍)做完这道题再来做食物链可能更好理解~~(双倍经验)~~ ## 边带权 依然从一道老掉牙的题目开始 [银河英雄传说](https://www.luogu.org/problemnew/show/P1196) 因为在并查集中存在着父子关系,所以也就有了树的形态,整个并查集数组就可以看成由若干棵树组成的森林,对于每个点维护一个数组d[]保存节点x到父亲节点fa[]之间的边权。 稍微放一下伪代码(逃: ``` int get(int x){ if(x==fa[x])return x; int f=fa[x]; fa[x]=get(fa[x]); dis[x]+=dis[f];//与祖先的距离 size[x]=size[fa[x]];//这个集合的大小 return fa[x]; } ``` 因为每次路径压缩后这个点都会直接指向祖先,那么,不妨直接利用路径压缩来更新d[](就是将维护数组的过程放在路径压缩中),这样,对于每一次询问祖先都能更新对应的边权,在最后调用结果的时候重新更新一遍就能保证其正确性。 理解了的话就再来一道$QwQ$ [叠积木](https://www.luogu.org/problemnew/show/P2342)(二倍经验) 代码什么的我相信你会写 ~~(不放代码就跑真开心~)~~ ## 并查集求环 由于并查集能维护父子关系,所以我们也可以将它运用到图论中,比如这道题[信息传递](https://www.luogu.org/problemnew/show/P2661),对于一个环,势必有一个点的父亲是他的子孙节点,(画个图很好理解的),如果发现将要成为自己父亲的节点是自己几代之后的子孙,这就说明有环出现了,用边带权并查集维护儿子是哪一代就可以求出环的大小,就可以进一步求最大环、最小环之类的东西啦,当然这只是并查集思路,这类题目还有另一种解法——$tarjan$(强烈安利)~~就是裸的缩点~~。 好的,如果理解了就来看下这道加强版[在农场万圣节](https://www.luogu.org/problemnew/show/P2921) ~~又不放代码啦啦啦~~ 特意去学了下可持久化并查集,发现她和并查集并没有很大的关系,所以这里不介绍啦(并不是偷懒) 以上,希望我的讲解能对大家有帮助 qwq 最后希望大家 noip2018 rp++! ~~估计发出来的时候我已经退役了~~ 猛然发现之前自己做的图片挂了,感谢好心的管理大大帮我配图$QAQ$~~虽然做之前那个图片我很用心的,好可惜嘤嘤嘤~~(怎么感觉管理大大的图片风格很诡异2333