基本概念
并查集,是用来解决连通性问题的一个手段。连通性是双向可传递的。
打个比方,假设一个人只会感染一种流感病毒, 有一群流感患者,如果张三和李四携带的是同一种病毒,李四和王五携带的又是同一种,那么张三和王五携带就是同一种。
换个说法, 已知熊大携带了A型病毒 , 熊大和狮二的病毒相同, 又知道鹿三的病毒是狮二传染的,所以鹿三的病毒也是A型病毒。
并查集两大功能,一个是合并,一个是查找, 合并的是集合, 查找的是属于哪个集合。 通过连通性找到源头,这就是查, 两个元素具有连通性,那么他们所在的集合其实就是同一个集合,就可以合并成一个集合。
用门派和师徒或者令牌来说一下,基本的并查集模板。
quick-find
一开始大家都是各练各的,有一天,路人壬看见你在练金刚基础拳法,于是对你说,兄弟原来是同门啊。然后你就知道了你和路人壬是同一派的。后来,你又遇上一个练伏虎拳的,路人壬告诉你,他也是同门。所以金刚拳和伏虎拳同门。你们觉得应该做一个门派信物,证明你们都是金刚伏虎门。 直到遇上达摩院的人,他们告诉你,你们的功夫都出自达摩院,然后给你们发了达摩院的令牌,你们就都换上达摩院的令牌了。后面只要一掏令牌,就知道这个人是属于哪个门派的。
通过令牌验明身份就是查, 换令牌(颜色)就是并。
上面的用令牌识别身份,遇到门派合并换令牌,就是并查集模板中的 quickFind,又被称为染色法。染色就是不同的门派令牌颜色不同, 合并的时候把令牌染成同一种颜色。
class QuickFind{
constructor(n){
/*刚开始每个人带带着自己独一无二的令牌 */
this.fa = new Array(n+1).map((v,i)=> i)
this.size = n
}
find(i){
return this.fa[i]
}
merge(i, j){
if(this.fa[i] === this.fa[j]){ return }
for(let n = 1;n < this.size; n++){
/* 因为 i 和 j 要合成一个门派, 所以让 j那一群人都佩戴 和i一样的令牌 */
if(this.fa[n] === this.fa[i]){
this.fa[n] = this.fa[j]
}
}
}
}
快和慢是相对的,这个染色法叫quick-find,就是因为它查找(所在集合)的速度比另一个要快。但是,染色法的合并比较慢,它需要把另一个集合中全部人的令牌都给染一遍。这里可以做个小小的优化。
那就是,记录各门派的人数,在合并门派的时候,让人少的并入人多的那边。这样需要染色的令牌就会相对少些。这就是人多势众啊。 不过,按上面那种写法的话,这种优化是没啥用的,因为每次换令牌,不是,现在是给令牌染色,都需要把全部武林人士过一遍。
quick-union
这个quick-union 就是快速合并。按江湖规矩,姑且称之为拜山头法(本来想写拜师法,但是想想这收徒收的太随意了点。还是换成便宜老大吧)。意思就是,刚开始,每个人都是自己的老大。后面遇到同门了,要统一了,就直接让以一方的老大拜另一方的老大为老大,这样就是只涉及一个最高层的变动。这合并速度,是不是快的飞起。
这种方式查找就要逐级往上查找。就比如要查路人庚的门派, 路人庚就说找我老大去,我不知道。结果他老大的老大的老大才是掌门,才知道是什么门派。这查找效率,一级一级的比染色法慢多了。
class QuickUnion{
constructor(){
this.fa = new Array(n+1).map((v,i)=> i) // 每个人都是自己的掌门
this.size = new Array(n+1),fill(1) // 每个门派初始人数1
}
find(i){
/* 找当家的 找师傅有点。。。 */
if(this.fa[i] === i) { return i}
while(this.fa[i] !== i){
i = this.fa[i] ;
}
return i
}
merge(i,j){
/*快速合并直接让 j的门主 拜入i的门主的门下 */
if(this.fa[i] === this.fa[j]){ return }
this.fa[this.fa[j]] = this.fa[i]
}
}
查找慢也是可以优化的。优化方法之一,还是人多势众。如果白虎门和黑虎门要合并, 白虎门人数多, 白虎的人会愿意自己头上莫名其妙多了个顶头上司吗? 所以,还是合并的时候,让人少的那一方的老大去拜人多的那一方的老大为老大,这样才能服众啊。
class Unionset3{//weighted-quick-union
constructor(n){
this.boss = new Array(n+1)// 开始每个人都是自己的掌门
this.size = new Array(n+1).fill(1)//各门派人数,初始1人
}
find(x){
if(this.boss[x] === x){
return x
}
return this.find( this.boss[x])
}
merge(a,b){
let ba = this.find(a),bb = this.find(b)
if(ba === bb) return
if(this.size(ba)> this.size(bb)){// 如果a的门派人多势众,也可能势均力敌
this.boss[bb] = ba //让b派的掌门认A派的掌门为师
this.size[bb] += this.size(ba) //b门派的人数增加了k
}else{// a 门派架不住b门派人多
this.boss[ba] = bb // a派掌门带头拜入b派的山门
this.size[ba] +=this.size(bb)//
}
}
}
另一种优化方法就是,路径压缩。就是在查找自己的顶头大哥之后,直接拜顶头大哥为大哥。已经知道顶头大哥是谁了,下次就不用再通过中间人去找了,直接升级。
class UnionSet4 {
constructor(n){
this.sup = new Array(n)
this.sup.forEach((v,i,arr)=>{
arr[i] = i
})
this.size = new Array(n).fill(1)
}
find(x){// 已经知道顶头大哥是谁了,直接拜顶头大哥为大哥
// return this.sup[x] = (this.sup[x] === x? x : this.find(this.sup[x]))
this.sup[x] = this.find(x)
return this.sup[x] === x? x:this.find(this.sup[x])
}
merge(a,b){
// 路径压缩
// let fa = this.find(a),fb = this.find(b)
// if(fa ===fb)return
this.sup[this.find(a)] = this.find(b)
}
}
最终极简版
class UnionSet {
constructor(n){
this.sup = new Array(n+1).fill().map((v ,i)=> i)// map方法会跳过数组空位 不是逻辑空
this.size = new Array(n).fill(1) // 每个集合初始元素数量1
this.dec= 0 ; // 执行合并的次数
}
find(x){// 查询时优化路径
return this.sup[x] = (this.sup[x] === x? x:this.find(this.sup[x]))
}
merge(a,b){
let fa = this.find(a),fb = this.find(b)
if(fa ===fb)return
this.size[fa] <this.size[fb]? this.sup[fa] = fb: this.sup[fb] = fa ;
this.dec++
}
}
并查集是用来解决连通性问题的一种数据结构,但是具体实现不一定就是上面那样,核心就是连通性是双向可传递的,并·查两种操作。