并查集 Java實(shí)現(xiàn)

算法題目 力扣 <連通網(wǎng)絡(luò)的操作次數(shù)>
知乎博客-算法學(xué)習(xí)筆記(1) : 并查集

前言

最近在學(xué)習(xí)中遇到這樣一道題(如下所示), 在評(píng)論區(qū)一片"并查集"飄過(guò), "并查集"是什么? 這不是典型的"親戚"問(wèn)題嗎? 大學(xué)時(shí)就學(xué)過(guò), 運(yùn)用"深度優(yōu)先遍歷"方法去解就好了. 那 "并查集" 是什么?

連通網(wǎng)絡(luò)的操作次數(shù)

用以太網(wǎng)線纜將 n 臺(tái)計(jì)算機(jī)連接成一個(gè)網(wǎng)絡(luò)邢疙,計(jì)算機(jī)的編號(hào)從 0 到 n-1暖释。線纜用 connections 表示取募,其中 connections[i] = [a, b] 連接了計(jì)算機(jī) a 和 b悴能。

網(wǎng)絡(luò)中的任何一臺(tái)計(jì)算機(jī)都可以通過(guò)網(wǎng)絡(luò)直接或者間接訪問(wèn)同一個(gè)網(wǎng)絡(luò)中其他任意一臺(tái)計(jì)算機(jī)锥余。

給你這個(gè)計(jì)算機(jī)網(wǎng)絡(luò)的初始布線 connections刁品,你可以拔開任意兩臺(tái)直連計(jì)算機(jī)之間的線纜辱姨,并用它連接一對(duì)未直連的計(jì)算機(jī)誊酌。請(qǐng)你計(jì)算并返回使所有計(jì)算機(jī)都連通所需的最少操作次數(shù)蹦漠。如果不可能椭员,則返回 -1 。

示例 1:

image

輸入:n = 4, connections = [[0,1],[0,2],[1,2]]
輸出:1
解釋:拔下計(jì)算機(jī) 1 和 2 之間的線纜笛园,并將它插到計(jì)算機(jī) 1 和 3 上隘击。

示例 2:

image

輸入:n = 6, connections = [[0,1],[0,2],[0,3],[1,2],[1,3]]
輸出:2

簡(jiǎn)介

并查集主要用于解決一些元素分組的問(wèn)題。它管理一系列不相交的集合研铆,并支持兩種操作:

  • 合并(Union):把兩個(gè)不相交的集合合并為一個(gè)集合埋同。
  • 查詢(Find):查詢兩個(gè)元素是否在同一個(gè)集合中。

當(dāng)然棵红,這樣的定義未免太過(guò)學(xué)術(shù)化凶赁,看完后恐怕不太能理解它具體有什么用。所以我們先來(lái)看看并查集最直接的一個(gè)應(yīng)用場(chǎng)景:親戚問(wèn)題逆甜。

題目背景
若某個(gè)家族人員過(guò)于龐大虱肄,要判斷兩個(gè)是否是親戚,確實(shí)還很不容易交煞,現(xiàn)在給出某個(gè)親戚關(guān)系圖咏窿,求任意給出的兩個(gè)人是否具有親戚關(guān)系。
題目描述
規(guī)定:x和y是親戚错敢,y和z是親戚翰灾,那么x和z也是親戚缕粹。如果x,y是親戚,那么x的親戚都是y的親戚纸淮,y的親戚也都是x的親戚平斩。
輸入格式
第一行:三個(gè)整數(shù)n,m,p,(n<=5000,m<=5000,p<=5000)咽块,分別表示有n個(gè)人绘面,m個(gè)親戚關(guān)系,詢問(wèn)p對(duì)親戚關(guān)系侈沪。
以下m行:每行兩個(gè)數(shù)Mi揭璃,Mj,1<=Mi亭罪,Mj<=N瘦馍,表示Mi和Mj具有親戚關(guān)系。
接下來(lái)p行:每行兩個(gè)數(shù)Pi应役,Pj情组,詢問(wèn)Pi和Pj是否具有親戚關(guān)系。
輸出格式
P行箩祥,每行一個(gè)’Yes’或’No’院崇。表示第i個(gè)詢問(wèn)的答案為“具有”或“不具有”親戚關(guān)系。

這其實(shí)是一個(gè)很有現(xiàn)實(shí)意義的問(wèn)題袍祖。我們可以建立模型底瓣,把所有人劃分到若干個(gè)不相交的集合中,每個(gè)集合里的人彼此是親戚蕉陋。為了判斷兩個(gè)人是否為親戚捐凭,只需看它們是否屬于同一個(gè)集合即可。因此寺滚,這里就可以考慮用并查集進(jìn)行維護(hù)了柑营。

并查集的引入

并查集的重要思想在于屈雄,用集合中的一個(gè)元素代表集合村视。我曾看過(guò)一個(gè)有趣的比喻,把集合比喻成幫派酒奶,而代表元素則是幫主蚁孔。接下來(lái)我們利用這個(gè)比喻,看看并查集是如何運(yùn)作的惋嚎。

image

最開始杠氢,所有大俠各自為戰(zhàn)。他們各自的幫主自然就是自己另伍。(對(duì)于只有一個(gè)元素的集合鼻百,代表元素自然是唯一的那個(gè)元素)

現(xiàn)在1號(hào)和3號(hào)比武绞旅,假設(shè)1號(hào)贏了(這里具體誰(shuí)贏暫時(shí)不重要),那么3號(hào)就認(rèn)1號(hào)作幫主(合并1號(hào)和3號(hào)所在的集合温艇,1號(hào)為代表元素)因悲。

image

現(xiàn)在2號(hào)想和3號(hào)比武(合并3號(hào)和2號(hào)所在的集合),但3號(hào)表示勺爱,別跟我打晃琳,讓我?guī)椭鱽?lái)收拾你(合并代表元素)。不妨設(shè)這次又是1號(hào)贏了琐鲁,那么2號(hào)也認(rèn)1號(hào)做幫主卫旱。

image

現(xiàn)在我們假設(shè)4、5围段、6號(hào)也進(jìn)行了一番幫派合并顾翼,江湖局勢(shì)變成下面這樣:

image

現(xiàn)在假設(shè)2號(hào)想與6號(hào)比,跟剛剛說(shuō)的一樣奈泪,喊幫主1號(hào)和4號(hào)出來(lái)打一架(幫主真辛苦氨┕埂)。1號(hào)勝利后段磨,4號(hào)認(rèn)1號(hào)為幫主取逾,當(dāng)然他的手下也都是跟著投降了。

img

好了苹支,比喻結(jié)束了砾隅。如果你有一點(diǎn)圖論基礎(chǔ),相信你已經(jīng)覺(jué)察到债蜜,這是一個(gè)狀的結(jié)構(gòu)晴埂,要尋找集合的代表元素,只需要一層一層往上訪問(wèn)父節(jié)點(diǎn)(圖中箭頭所指的圓)寻定,直達(dá)樹的根節(jié)點(diǎn)(圖中橙色的圓)即可儒洛。根節(jié)點(diǎn)的父節(jié)點(diǎn)是它自己。我們可以直接把它畫成一棵樹:

image

用這種方法狼速,我們可以寫出最簡(jiǎn)單版本的并查集代碼琅锻。

最簡(jiǎn)單的并查集

存儲(chǔ)結(jié)構(gòu)

class UnionFind {
    int[] parent;
}

初始化

假如有編號(hào)為1, 2, 3, ..., n的n個(gè)元素,我們用一個(gè)數(shù)組parent[]來(lái)存儲(chǔ)每個(gè)元素的父節(jié)點(diǎn)(因?yàn)槊總€(gè)元素有且只有一個(gè)父節(jié)點(diǎn)向胡,所以這是可行的)恼蓬。一開始,我們先將它們的父節(jié)點(diǎn)設(shè)為自己僵芹。

public UnionFind(int n) {
    parent = new int[n];
    for (int i = 0; i < n; i++) {
        parent[i] = i;
    }
}

查詢

我們用遞歸的寫法實(shí)現(xiàn)對(duì)代表元素的查詢:一層一層訪問(wèn)父節(jié)點(diǎn)处硬,直至根節(jié)點(diǎn)(根節(jié)點(diǎn)的標(biāo)志就是父節(jié)點(diǎn)是本身)。要判斷兩個(gè)元素是否屬于同一個(gè)集合拇派,只需要看它們的根節(jié)點(diǎn)是否相同即可荷辕。

public int find(int x) {
    if (parent[x] == x) {
        return x;
    } else {
        return find(parent[x]);
    }
}

合并

合并操作也是很簡(jiǎn)單的凿跳,先找到兩個(gè)集合的代表元素,然后將前者的父節(jié)點(diǎn)設(shè)為后者即可疮方。當(dāng)然也可以將后者的父節(jié)點(diǎn)設(shè)為前者拄显,這里暫時(shí)不重要。本文末尾會(huì)給出一個(gè)更合理的比較方法案站。

public void merge(int x, int y) {
    int i = find(x);
    int j = find(y);
    if (i == j) {
        return;
    }
    parent[i] = j;
}

一個(gè)簡(jiǎn)單的并查集功能就完成了, 試一下

/**
 * 判斷節(jié)點(diǎn)x和節(jié)點(diǎn)y是不是有聯(lián)系
 *
 * @param n           節(jié)點(diǎn)數(shù)量
 * @param connections 節(jié)點(diǎn)間關(guān)系 : 例子 connections[i] = [a, b] 表示 a 和 b 是親戚躬审。
 * @param x           節(jié)點(diǎn)x
 * @param y           節(jié)點(diǎn)y
 * @return x和節(jié)點(diǎn)y是不是有聯(lián)系
 */
public boolean makeConnected(int n, int[][] connections, int x, int y) {
    UnionFind uf = new UnionFind(n);
    for (int[] conn : connections) {
        uf.merge(conn[0], conn[1]);
    }
    return uf.find(x) == uf.find(y);
}

改進(jìn)

路徑壓縮

最簡(jiǎn)單的并查集效率是比較低的。例如蟆盐,來(lái)看下面這個(gè)場(chǎng)景:

image

現(xiàn)在我們要merge(2,3)承边,于是從2找到1,parent[1]=3石挂,于是變成了這樣:

image

然后我們又找來(lái)一個(gè)元素4博助,并需要執(zhí)行merge(2,4):

image

從2找到1,再找到3痹愚,然后fa[3]=4富岳,于是變成了這樣:

image

大家應(yīng)該有感覺(jué)了,這樣可能會(huì)形成一條長(zhǎng)長(zhǎng)的拯腮,隨著鏈越來(lái)越長(zhǎng)窖式,我們想要從底部找到根節(jié)點(diǎn)會(huì)變得越來(lái)越難。

怎么解決呢动壤?我們可以使用路徑壓縮的方法萝喘。既然我們只關(guān)心一個(gè)元素對(duì)應(yīng)的根節(jié)點(diǎn),那我們希望每個(gè)元素到根節(jié)點(diǎn)的路徑盡可能短琼懊,最好只需要一步阁簸,像這樣:

image

其實(shí)這說(shuō)來(lái)也很好實(shí)現(xiàn)。只要我們?cè)诓樵兊倪^(guò)程中哼丈,把沿途的每個(gè)節(jié)點(diǎn)的父節(jié)點(diǎn)都設(shè)為根節(jié)點(diǎn)即可启妹。下一次再查詢時(shí),我們就可以省很多事醉旦。這用遞歸的寫法很容易實(shí)現(xiàn):

合并(路徑壓縮)

public int find(int x) {
    if (parent[x] == x) {
        return x;
    } else {
        parent[x] = find(parent[x]);
        return parent[x];
    }
}

按秩合并

有些人可能有一個(gè)誤解饶米,以為路徑壓縮優(yōu)化后,并查集始終都是一個(gè)菊花圖(只有兩層的樹的俗稱)髓抑。但其實(shí)咙崎,由于路徑壓縮只在查詢時(shí)進(jìn)行优幸,也只壓縮一條路徑吨拍,所以并查集最終的結(jié)構(gòu)仍然可能是比較復(fù)雜的。例如网杆,現(xiàn)在我們有一棵較復(fù)雜的樹需要與一個(gè)單元素的集合合并:

image

假如這時(shí)我們要merge(7,8)羹饰,如果我們可以選擇的話伊滋,是把7的父節(jié)點(diǎn)設(shè)為8好,還是把8的父節(jié)點(diǎn)設(shè)為7好呢队秩?

當(dāng)然是后者笑旺。因?yàn)槿绻?的父節(jié)點(diǎn)設(shè)為8,會(huì)使樹的深度(樹中最長(zhǎng)鏈的長(zhǎng)度)加深馍资,原來(lái)的樹中每個(gè)元素到根節(jié)點(diǎn)的距離都變長(zhǎng)了筒主,之后我們尋找根節(jié)點(diǎn)的路徑也就會(huì)相應(yīng)變長(zhǎng)。雖然我們有路徑壓縮鸟蟹,但路徑壓縮也是會(huì)消耗時(shí)間的乌妙。而把8的父節(jié)點(diǎn)設(shè)為7,則不會(huì)有這個(gè)問(wèn)題建钥,因?yàn)樗鼪](méi)有影響到不相關(guān)的節(jié)點(diǎn)藤韵。

image

這啟發(fā)我們:我們應(yīng)該把簡(jiǎn)單的樹往復(fù)雜的樹上合并,而不是相反熊经。因?yàn)檫@樣合并后泽艘,到根節(jié)點(diǎn)距離變長(zhǎng)的節(jié)點(diǎn)個(gè)數(shù)比較少。

我們用一個(gè)數(shù)組rank[]記錄每個(gè)根節(jié)點(diǎn)對(duì)應(yīng)的樹的深度(如果不是根節(jié)點(diǎn)镐依,其rank相當(dāng)于以它作為根節(jié)點(diǎn)的子樹的深度)匹涮。一開始,把所有元素的rank()設(shè)為1槐壳。合并時(shí)比較兩個(gè)根節(jié)點(diǎn)焕盟,把rank較小者往較大者上合并机错。路徑壓縮和按秩合并如果一起使用消玄,時(shí)間復(fù)雜度接近 [圖片上傳失敗...(image-98c7ec-1611629139843)] ,但是很可能會(huì)破壞rank的準(zhǔn)確性炎咖。

存儲(chǔ)結(jié)構(gòu)

class UnionFind {
    int[] parent;
    int[] rank;
}

初始化(按秩合并)

public UnionFind(int n) {
    parent = new int[n];
    for (int i = 0; i < n; i++) {
        parent[i] = i;
    }
    rank = new int[n];
    Arrays.fill(rank, 1);
}

合并(按秩合并)

public void merge(int x, int y) {
    int i = find(x);
    int j = find(y);
    if (i == j) {
        return;
    }
    if (rank[i] > rank[j]) {
        parent[j] = i;
    } else {
        parent[i] = j;
    }
    if (rank[i] == rank[j]) {
        rank[i]++;
    }
}

為什么深度相同绍哎,新的根節(jié)點(diǎn)深度要+1来农?如下圖,我們有兩個(gè)深度均為2的樹崇堰,現(xiàn)在要merge(2,5):

image

這里把2的父節(jié)點(diǎn)設(shè)為5沃于,或者把5的父節(jié)點(diǎn)設(shè)為2,其實(shí)沒(méi)有太大區(qū)別海诲。我們選擇前者繁莹,于是變成這樣:

image

顯然樹的深度增加了1。另一種合并方式同樣會(huì)讓樹的深度+1特幔。

解題 連通網(wǎng)絡(luò)的操作次數(shù)

public class Solution {


    public int makeConnected(int n, int[][] connections) {
        if (connections.length < n - 1) {
            return -1;
        }

        UnionFind uf = new UnionFind(n);
        for (int[] conn : connections) {
            uf.merge(conn[0], conn[1]);
        }

        return uf.outlierNum - 1;
    }
}

class UnionFind {

    int[] parent;
    int[] rank;
    int outlierNum;

    public UnionFind(int n) {
        parent = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
        rank = new int[n];
        Arrays.fill(rank, 1);
        outlierNum = n;
    }

    public int find(int x) {
        if (parent[x] == x) {
            return x;
        } else {
            parent[x] = find(parent[x]);
            return parent[x];
        }
    }

    public void merge(int x, int y) {
        int i = find(x);
        int j = find(y);
        if (i == j) {
            return;
        }
        if (rank[i] > rank[j]) {
            parent[j] = i;
        } else {
            parent[i] = j;
        }
        if (rank[i] == rank[j]) {
            rank[i]++;
        }
        outlierNum--;
    }

}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末咨演,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蚯斯,更是在濱河造成了極大的恐慌薄风,老刑警劉巖饵较,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異遭赂,居然都是意外死亡循诉,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門撇他,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)茄猫,“玉大人,你說(shuō)我怎么就攤上這事困肩∧即” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵僻弹,是天一觀的道長(zhǎng)阿浓。 經(jīng)常有香客問(wèn)我,道長(zhǎng)蹋绽,這世上最難降的妖魔是什么芭毙? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮卸耘,結(jié)果婚禮上退敦,老公的妹妹穿的比我還像新娘。我一直安慰自己蚣抗,他們只是感情好侈百,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著翰铡,像睡著了一般钝域。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锭魔,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天例证,我揣著相機(jī)與錄音,去河邊找鬼迷捧。 笑死织咧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的漠秋。 我是一名探鬼主播笙蒙,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼庆锦!你這毒婦竟也來(lái)了捅位?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绿渣,沒(méi)想到半個(gè)月后朝群,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體燕耿,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡中符,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了誉帅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淀散。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蚜锨,靈堂內(nèi)的尸體忽然破棺而出档插,到底是詐尸還是另有隱情,我是刑警寧澤亚再,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布郭膛,位于F島的核電站,受9級(jí)特大地震影響氛悬,放射性物質(zhì)發(fā)生泄漏则剃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一如捅、第九天 我趴在偏房一處隱蔽的房頂上張望棍现。 院中可真熱鬧,春花似錦镜遣、人聲如沸己肮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)谎僻。三九已至,卻和暖如春寓辱,著一層夾襖步出監(jiān)牢的瞬間戈稿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工讶舰, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鞍盗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓跳昼,卻偏偏與公主長(zhǎng)得像般甲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鹅颊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 并查集被很多OIer認(rèn)為是最簡(jiǎn)潔而優(yōu)雅的數(shù)據(jù)結(jié)構(gòu)之一敷存,主要用于解決一些元素分組的問(wèn)題。它管理一系列不相交的集合,并...
    Pecco閱讀 360評(píng)論 0 0
  • 并查集也叫做不相交集合(Disjoint Set),這種數(shù)據(jù)結(jié)構(gòu)主要用來(lái)快速的判斷兩個(gè)集合是否有交集的問(wèn)題锚烦,或者說(shuō)...
    Bel李玉閱讀 594評(píng)論 0 1
  • 前言 并查集(Disjoint-set) 的代碼非常簡(jiǎn)潔觅闽,但是功能卻很強(qiáng)大。關(guān)于并查集涮俄,這里有一篇文章超有愛的并查...
    程點(diǎn)閱讀 23,198評(píng)論 1 20
  • 并查集 并查集實(shí)際上就是并集和查集的過(guò)程蛉拙。集(集合)可以理解為一棵樹,即一個(gè)根結(jié)點(diǎn)連著無(wú)數(shù)個(gè)子節(jié)點(diǎn)彻亲。 例題解析 輸...
    VGSemir閱讀 1,560評(píng)論 0 1
  • //本文首先發(fā)表于博客園孕锄,點(diǎn)擊這里查看我的博客。廢話不多說(shuō)苞尝,直接看題: 一看這道題畸肆,我就有了思路:既然這道題身在圖...
    gzr666閱讀 419評(píng)論 0 1