并查集UnionFind

文章参考自博客:https://www.cnblogs.com/SeaSky0606/p/4752941.html

1、前言

为了便于引入算法,下面我们假设一个场景:

假设现在有A,B两人素不相识,但A通过熟人甲,甲通过熟人乙,乙通过熟人丙,丙通过熟人丁,而丁又刚好与B是熟人。就这样,A通过一层一层的人际关系最后认识了B。

基于以上介绍的“关系网”,现在给出一道思考题:13亿中国人当中一共有几个“关系网”呢?

如果用图论的知识来说,就是图中有多少个连通子图呢?

2、Union-Find初探

是的,想到1,300,000,000这个数字,或许此刻你大脑已经懵了。那好,我们就先从小数据分析:

从上图中,其实很好理解。初始每个人都是单独的一个“点”,用科学语言,我们把它描述为“连通分量”。随着一个一个关系的确立,即点与点之间的连接,每连接一次,总连通分量数即减1(理解算法的关键点之一)。最后的“关系网”几乎可以很轻易地数出来。所以,只要你把所有国人两两之间的联系给出,然后不断连线,连线,...,最后再统计一下不就完事儿了麽~

问题是:怎么存储点的信息?点与点怎么连,怎么判断该不该连?

因此,我们需要维护2个变量,其中一个变量count表示实时的连通分量数,另一个变量可以用来存储具体每一个点所属的连通分量。因为不需要存储复杂的信息。这里我们选常用的数组 id[N] 存储即可。然后,我们需要2个函数find(int x)和union(int p,int q)。前者返回点“x”所属于的连通分量,后者将p,q两点进行连接。注意,所谓的连接,其实可以简单的将p的连通分量值赋予q或者将q的连通分量值赋予p,即:

id[p]=q 或者id[q]=p。

有了上面的分析,我们就可以牛刀小试了。且看Java代码实现第一版。

package UnionFind;

import java.util.Scanner;

public class BasicUnionFInd {
    int count; //连通分量数
    int[] id; //每个数所属的连通分量

    public BasicUnionFInd(int N){
        count = N;
        id= new int[N];
        for(int i=0;i<N;i++){
            id[i] = I;
        }
    }

    //返回连通分量数
    public int getCount(){
        return this.count;
    }

    //查找x所属的连通分量
    public int find(int x){
        return id[x];
    }

    //将两个点进行合并 
    public void union(int p,int q){
        int pID = find(p);
        int qID = find(q);
        for(int i=0;i<id.length;i++){
            if(find(i)==pID){
                id[i] = qID;
            }
        }
        count--;
    }

    //判断两个点是否连通
    public boolean connected(int p,int q){
        return find(p) == find(q);
    }

    public static void main(String[] args){
        Scanner input = new Scanner(System.in);
        System.out.println("please input the count of total numbers");
        int N = input.nextInt();
        BasicUnionFInd buf = new BasicUnionFInd(N);
        System.out.println("please input pairs,input exit to end");
        while(input.hasNext()){
            String pair = input.next();
            if(pair.equals("exit")) break;
            int p = Integer.parseInt(pair.split("-")[0]);
            int q = Integer.parseInt(pair.split("-")[1]);
            if(buf.connected(p,q)) continue;
            buf.union(p,q);
        }

        System.out.println("总的连通分量数是:" + buf.getCount());

    }
}

代码的输入输出为:

可以看到,find()操作的时间复杂度为:O(l),Union的时间复杂度为:O(N)。因为算法可以非常高效地实现find(),所以我们也把它称为“quick-find”算法。

3.Union-find进阶

仔细一想,我们上面再进行union()连接操作时,实际上就是一个进行暴力“标记”的过程,即把所有连通分量id跟点q相同的点找出来,然后全部换成p的id。算法本身没有错,但是这样的代价太高了,得想办法优化~

因此,这里引入了一个抽象的“树”结构,即初始时每个点都是一棵独立的树,所有的点构成了一个大森林。每一次连接,实际上就是两棵树的合并。通过,不断的合并,合并,再合并最后长成了一棵棵的大树。

package UnionFind;

import java.util.Scanner;

public class UnionFind_v1 {
    int count; //连通分量数
    int[] id; //每个数所属的连通分量

    public UnionFind_v1(int N){
        count = N;
        id= new int[N];
        for(int i=0;i<N;i++){
            id[i] = I;
        }
    }

    //返回连通分量数
    public int getCount(){
        return this.count;
    }

    //查找x所属的连通分量
    public int find(int x){
        while(x!=id[x]) x=id[x];
        return x;
    }

    //将两个点进行合并
    public void union(int p,int q){
        int pID = find(p);
        int qID = find(q);
        if(pID==qID) return;
        id[qID] = pID;
        count--;
    }

    //判断两个点是否连通
    public boolean connected(int p,int q){
        return find(p) == find(q);
    }

    public static void main(String[] args){
        Scanner input = new Scanner(System.in);
        System.out.println("please input the count of total numbers");
        int N = input.nextInt();
        BasicUnionFInd buf = new BasicUnionFInd(N);
        System.out.println("please input pairs,input exit to end");
        while(input.hasNext()){
            String pair = input.next();
            if(pair.equals("exit")) break;
            int p = Integer.parseInt(pair.split("-")[0]);
            int q = Integer.parseInt(pair.split("-")[1]);
            if(buf.connected(p,q)) continue;
            buf.union(p,q);
        }

        System.out.println("总的连通分量数是:" + buf.getCount());

    }
}

代码的输入输出为:

利用树本身良好的连通性,我们算法仅需要O(l)时间代价进行union()操作,但此时find()操作的时间代价有所增加。结合本算法对quick-find()的优化,我们把它称为“quick-union”算法。

4.Union-Find再进阶

表面上,上述引入“树”结构的算法时间复杂度由原来的O(N)改进为O(lgN)。但是,不要忽略了这样一种极端情况,即每连接一个点之后,树在不断往下生长,最后长成一棵“秃树”(没有任何树枝)。

为了不让我们前面做的工作白费,必须得采取某些措施避免这种恶劣的情况给我们算法带来的巨大代价。所以...

是的,或许你已经想到了,就是在两棵树进行连接之前做一个判断。每一次都优先选择将小树合并到大树下面,这样子树的高度不变,能避免树一直往下增长了!下图中,数据增加了“6-2”的一条连接,得知以“2”为根节点的树比“6”的树大,对比(f)和(g)两种连接方式,我们最优选择应该是(g),即把小树并到大树下。

基于此,我们还得引入一个变量对以每个结点为根节点的树的大小进行维护,具体我们以sz[i]表示i结点代表的树(或子树)的结点数作为它的大小,初始sz[i]=1。因为现在的每一个结点都有了权重,所以我们也把这种树结构称为“加权树”,本算法称为“weightedUnionFind”。

package UnionFind;

import java.util.Scanner;

public class WeightedUnionFind {
    int count; //连通分量数
    int[] id; //每个数所属的连通分量
    int[] sz;

    public WeightedUnionFind(int N){
        count = N;
        id= new int[N];
        sz = new int[N];
        for(int i=0;i<N;i++){
            id[i] = I;
            sz[i] = 1;
        }
    }

    //返回连通分量数
    public int getCount(){
        return this.count;
    }

    //查找x所属的连通分量
    public int find(int x){
        while(x!=id[x]) x=id[x];
        return x;
    }

    //将两个点进行合并
    public void union(int p,int q){
        int pID = find(p);
        int qID = find(q);
        if(pID==qID) return;
        if(sz[p] < sz[q]){
            id[pID] = qID;
            sz[q] += sz[p];
        }
        else{
            id[qID] = pID;
            sz[p] += sz[q];
        }
        count--;
    }

    //判断两个点是否连通
    public boolean connected(int p,int q){
        return find(p) == find(q);
    }

    public static void main(String[] args){
        Scanner input = new Scanner(System.in);
        System.out.println("please input the count of total numbers");
        int N = input.nextInt();
        BasicUnionFInd buf = new BasicUnionFInd(N);
        System.out.println("please input pairs,input exit to end");
        while(input.hasNext()){
            String pair = input.next();
            if(pair.equals("exit")) break;
            int p = Integer.parseInt(pair.split("-")[0]);
            int q = Integer.parseInt(pair.split("-")[1]);
            if(buf.connected(p,q)) continue;
            buf.union(p,q);
        }

        System.out.println("总的连通分量数是:" + buf.getCount());

    }
}

代码的输入输出如下:

5总结

下面我们来比较一下算法的性能:

个人认为WeightedUF的union时间复杂度是O(1),原博客应该是写错了。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容