武破并查集

178 阅读4分钟

基本概念

并查集,是用来解决连通性问题的一个手段。连通性是双向可传递的。

打个比方,假设一个人只会感染一种流感病毒, 有一群流感患者,如果张三和李四携带的是同一种病毒,李四和王五携带的又是同一种,那么张三和王五携带就是同一种。
换个说法, 已知熊大携带了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++
    }

}

并查集是用来解决连通性问题的一种数据结构,但是具体实现不一定就是上面那样,核心就是连通性是双向可传递的,并·查两种操作

附相关力扣题解

省份数量
岛屿数量
等式方程的可满足性
冗余连接
连通网络