并查集(Union-Find Algorithm),看这一篇就够了

动态连接(Dynamic connectivity)的问题

所谓的动态连接问题是指在一组可能相互连接也可能相互没有连接的对象中,判断给定的两个对象是否联通的一类问题。这类问题可以有如下抽象:

  • 有一组构成不相交集合的对象
  • union: 联通两个对象
  • find: 返回两个对象之间是否存在一条联通的通路
    ˇ
    在使用union-find处理动态连接的问题时,我们一般将这一组对象抽象为一个数组。

对于这组对象,其中相互连接的一些对象构成的子集称为联通集。

算法目的:能够在如下条件下高效解决动态连接的问题

  • Union命令和Find命令可能交替被调用
  • 操作的总数M可能很大
  • 集合中的对象数目N可能很大

Quick find

数据结构:

  • 输入数组id[]的长度为N。且每一个对象最初的id都为其本身。
  • 当且仅当pq具有相同的idpq才是联通的。
  • id[]数组中存储对应对象所属的联通集的root的id。

算法:

  • Union:欲将pq相连,相当于合并包含p的联通集和包含q的联通集,也就是将所有idid[p]相同的对象的id改为id[q]
  • Find:检查pqid是否相同即可。

示例:

对于下表所示的对象集合,如果我们调用union(1,3),则需要将所有id2的对象的id改为4。经过这个操作之后,原先的两个联通集[1,2][3,4]如今成为了一个联通集。

|  i    | 0 | 1 | 2 | 3 | 4 |
| id[i] | 0 | 2 | 2 | 4 | 4 |

==> 

|  i    | 0 | 1 | 2 | 3 | 4 |
| id[i] | 0 | 4 | 4 | 4 | 4 |

Quick find的Java实现

public class QuickFind {
    int[] id;
    public QuickFind(int n) {
        id = new int[n];
        for (int i = 0; i < n; i++) {
            id[i] = i;
        }
    }

    public void union(int p, int q) {
        int pid = id[p];
        int qid = id[q];
        for (int i = 0; i < this.id.length; i++) {
            if (id[i] == pid) {
                id[i] = qid;
            }
        }
    }

    public boolean find(int p, int q) {
        return id[p] == id[q];
    }
}

时间复杂度分析

  • find()操作的时间复杂度为O(1)
  • union()操作的时间复杂度为O(N)

Quick union

显而易见,Quick find算法太慢了。如果我们想要重复调用union()N次,时间复杂度将为O(N^2)。那么我们如何优化其时间复杂度呢?

我们可以采用称为lazy approach的方法来进行优化。所谓的lazy approach,也就是我们在设计算法的时候,对于一个步骤尽量减少其工作量,直到我们不得不进行这些工作的时候才进行。对于优化Quick find算法而言,就是我们尽量减少union()中的工作量,知道我们在调用find()的时候再去补上之前偷懒没有做的工作。那么我们如何减少union()中的工作量呢?

答案是:直到有必要前,我们并不改变一个联通集中的每一个元素的id

Quick find算法中,我们每一次union()操作都会将一个联通集中的每一个元素的id改为联通集中root元素的id。现在我们将其改变为仅仅将新元素所属的联通集的root的id改为另一个元素所属的联通集的root的id。直到我们需要判断两个元素是否连通的时候,也就是调用find()的时候,我们就寻找两个元素所属的联通集的root id是否相同。

数据结构:

  • 输入数组id[]的长度为N。且每一个对象最初的id都为其本身。
  • 当且仅当pq具有相同的root idpq才是联通的。
  • id[]数组中存储相应对象的parent的id。
  • i的root为id[id[id[...id[i]...]]]

算法:

  • Union:欲将pq相连,也就是将q所属的联通集融合为p所属的联通集的root的子联通集,即将q所述的联通集的root的id改为p所属的联通集的root的id。
  • Find:检查pqroot id是否相同。

示例:

对于下表所示的对象集合,如果我们调用union(1,3),则需要将3所属的联通集的root的id改为1所属的联通集的root的id,也就是将id[4]改为2

|  i    | 0 | 1 | 2 | 3 | 4 |
| id[i] | 0 | 2 | 2 | 4 | 4 |

==> 

|  i    | 0 | 1 | 2 | 3 | 4 |
| id[i] | 0 | 2 | 2 | 4 | 2 |

Quick union的Java实现

public class QuickUnion{
    int[] id;
    
    public QuickUnion(int n) {
        this.id = new int[n];
                for (int i = 0; i < n; i++) {
                    id[i] = i;
                }
    }
    
    public void union(int p, int q) {
        int rootP = getRoot(p);
        int rootQ = getRoot(q);
        id[rootQ] = rootP;
    }
    
    public boolean find(int p, int q) {
        return getRoot(p) == getRoot(q);
    }
    
    private int getRoot(int i) {
        while (i != id[i]) {
            i = id[i];
        }
        return i;
    }
}

时间复杂度分析

  • find()操作的时间复杂度最坏情况下为O(N)
  • union()操作的时间复杂度最坏情况下为O(1)

Quick union的表现将随着我们不断调用union()构建联通集而变差。因为代表这个联通集的树越来越高,调用getRoot()的开销也就越来越大。

Weighed Quick Union with Path Compression

通过以上的分析,我们得到了一个稍快的算法Quick union,但其时间复杂度会随着联通集所对应的树越来越高而变差。我们是否可以进一步优化这个算法呢?

答案是可以的。既然其表现随着树的高度增长而变差,那么我们就需要找出一些方法来使联通集所构造的树更加扁平。通过以下两种方法,我们可以大大减少树的高度。

Weighed Quick union

Quick union为基础,我们额外利用一个sz[]保存每一个联通集中对象的数量。在调用union()的时候,我们总是把对象数目较少的联通集连接到对象数目较多的联通集中。通过这种方式,我们可以在一定程度上缓解树的高度太大的问题,从而改善Quick union的时间复杂度。

算法

  • Union:在Quick union的基础上,将较小的联通集并入较大的联通集中。并且在合并之后更新sz[]数组中对应的联通集的大小。
  • Find:与Quick union相同。

时间复杂度分析

  • find()操作的时间复杂度最坏情况下为O(lgN)
    原因在于我们每次都将包含对象较少的联通集连接到包含对象较大的联通集上,因此产生的联通集在最坏情况下的高度为O(lgN)
  • union()操作的时间复杂度最坏情况下为O(lgN)
    原因与find()相同。

Path compression

Quick union为基础,在寻找对象i所对应的联通集的root的过程之后,将中途所检查过的每一个对象对应的id都改为root(i)。如下面的例子所示:

Tree representation of path compression.png

在实际代码实现的时候,为简单起见,我们并不将所有检查过的对象的id都改为root(i),而是将每一个元素的id改为其parentid。这样虽然无法完全将树扁平化,但可以达到近似的优化效果。

算法

  • Union:在Quick union的基础上,每次在寻找某一个对象所对应的联通集的root的时候,将沿途遇到的每一个对象的id改为id[id[i]]。或者记录下root的id,用另一个循环来将沿途每一个对象的id改为root的id
  • Find:与Quick union相同。

时间复杂度分析

  • union():最坏情况下为O(lgN)
  • find():最坏情况下为O(lgN)

Weighed Quick Union with Path Compression时间复杂度分析

理论上,从一个完全不相连通的N个对象的集合开始,任意顺序的Munion()调用所需的时间为O(N+Mlg*N)

其中lg*N称为迭代对数(iterated logarithm)。实数的迭代对数是指须对实数连续进行几次对数运算后,其结果才会小于等于1。这个函数增加非常缓慢,可以视为近似常数(例如2^65535的迭代对数为5)。

因此我们可以认为Weighed Quick Union with Path Compression是一个线性时间的算法。

Weighed Quick Union with Path Compression的Java实现

public class WeighedQuickUnionWithPathCompression{
    int[] id;
    int[] sz;
    
    public WeighedQuickUnionWithPathCompression(int n) {
        this.id = new int[n];
        this.sz = new int[n];
        for (int i = 0; i < n; i++) {
            id[i] = i;
            sz[i] = 1;
        }
    }
    
    public void union(int p, int q) {
        int rootP = getRoot(p);
        int rootQ = getRoot(q);
        
        // weighed quick union
        if (sz[rootP] >= sz[rootQ]) {
            id[rootQ] = rootP;
            sz[rootP] += sz[rootQ];
        } else {
            id[rootP] = rootQ;
            sz[rootQ] += sz[rootP];
        }
    }
    
    public boolean find(int p, int q) {
        return getRoot(p) == getRoot(q);
    }
    
    private int getRoot(int i) {
        while (i != id[i]) {
            // path compression
            id[i] = id[id[i]];
            i = id[i];
        }
        return i;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容