日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

并查集入门

發布時間:2025/6/15 编程问答 10 豆豆
生活随笔 收集整理的這篇文章主要介紹了 并查集入门 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

并查集

并查集是一種樹型的高級數據結構,主要用于處理不想交集合的合并及查詢問題。它在計算機科學中有著廣泛的應用。例如求解最小生成樹(克魯斯卡爾算法)、親戚關系的判定、確定無向圖的連通子圖個數、最小公共祖先問題等,都要用到并查集。

集合

集合是數學中最基本的構造之一,將一組滿足某種性質的對象放在一起就形成了集合。集合中包含的對象稱為集合中的元素,集合中的元素是無序而且唯一的。常用大寫英文字母A、B、C等來表示集合,并用x∈A來表示x是集合A中的元素。

集合的并、交、差:

由A、B集合的全體元素組成的集合為A與B的并集,記作A∪B;A與B的公共元素組成的集合稱為A與B的交集,記作A∩B;屬于集合A而不屬于集合B的元素組成的集合稱為A減B的差,記作A-B.

集合中元素的存儲

  • 數組存儲數組一旦定義,其大小就固定不變。
  • 鏈表鏈表可以很容易的動態生成和釋放,所以增減節點是很方便的,完全不用考慮數組那樣的長度問題。刪除也很方便。但因為涉及到指針,實現起來很容易出錯。
  • vector它有數組的優點,而且又不必考慮數組那樣可能越界的情況。
  • 并查集的概念

    在某些應用中,我們要檢查兩個元素是否屬于同一個集合,或者將兩個不同的集合合并為一個集合。這是不相交集合經常處理的兩種操作:查找和合并,我們成為并查集。

    查找find:查找一個指定元素屬于哪個集合。對于判斷兩個元素是否屬于同一個集合是非常有用的。

    合并union:將兩個集合合并為1個集合。

    如何標示一個集合

    選擇集合中某個固定的元素作為集合的代表,讓它唯一的標識整個集合。一般來說,選取的代表是任意的。也就是說,到底選擇集合中的哪個元素作為它的代表是無關緊要的。

    樹的思想

    在并查集中,我們對于集合的表示利用樹的思想,一個集合可以看做一棵樹,樹根即代表該集合的標識。如果兩個集合在同一個樹中,則它們是同一個集合;合并兩個集合,即是對兩棵樹進行合并。

    Find(x)
    返回元素x所屬集合的代表.

    Query(x, y)
    詢問元素x和元素y是否在一個集合中。只需判斷find(x)和find(y)是否相等即可。如果相等,說明他們屬于同一個集合,否則它們不屬于同一個集合。

    Union(x, y)
    將包含元素x的集合(假設為Sx)和包含元素y的集合(假設為Sy)合并為一個新的集合(即這兩個集合的并集),所得到的并集可以用它的任何一個元素來做代表,但在實踐中,一般都是選擇Sx或者Sy的代表作為并集的代表。

    N個不同的元素分布在若干個互不相交集合中,需要進行一下3個操作:

  • 合并兩個集合
  • 查詢一個元素在哪個集合
  • 查詢兩個元素是否屬于同一個集合
  • 并查集操作示例

    OperationDisjoint sets
    初始狀態{a}{b}{c}ozvdkddzhkzd{e}{f}
    Merge(a,b){a,b}?{c}ozvdkddzhkzd{e}{f}
    Query(a,c)False
    Query(a,b)True
    Merge(b,e){a,b,e}?{c}ozvdkddzhkzd?{f}
    Merge(c,f){a,b,e}?{c,f}ozvdkddzhkzd??
    Query(a,e)True
    Query(c,b)False
    Merge(b,f){a,b,c,e,f}??ozvdkddzhkzd??
    Query(a,e)True
    Query(d,e)False

    土算法

    給集合編號

    ?

    ?

    {a}{b}{c}ozvdkddzhkzd{e}{f}
    ?123456
    Merge(a,b)113456
    Merge(b,e)113416
    Merge(c,f)113413
    Merge(b,f)111411

    Query(a,e) :檢查a,e的編號
    算法復雜度
    Query—O(1); Nerge—O(N)

    用樹結構表示集合

    Init:

    Merge(a,b):

    Merge(b,e):

    Merge(c,f):

    Merge(b,f):

    Mege(b,f):
    將f所在樹掛在b所在樹的直接子樹
    開設父親節點指示數組Par,Par[i]代表第i個元素的父親。若元素i是樹根,則Par[i] = i

    Query(b,f)
    簡單比較b和f所在的根節點是否相同

    缺點
    樹可能層次太深,以至于查樹根太慢

    Merge(d,c), Merge(c,b), Merge(b,a) …

    解決方案一:根據樹的層次進行合并
    1、每個節點(元素)維護一個Rank表示子樹最大可能高度
    2、較小Rank的樹連到較大Rank的樹的根部

    Link(x, y)
    1 2 3 4 5 6 7 //new code if (Rank[x] > Rank[y]) ??Par[y] = x; else ??Par[x] = y; ??if (Rank[x] == Rank[y]) ????Rank[y]++;
    1 2 //old code Par[y] = x;
    GET_PAR(a) 求a的根節點
    1 2 3 4 if (Par[a] == a) ??return a; else???? ??return GET_PAR(par[a]);
    Query(a,b) //O(logN)
    1 return GET_PAR(a) == GET_PAR(b);
    Merge(a,b) //O(logN)
    1 Link(GET_PAR(a), GET_PAR(b));

    改進方法二:路徑壓縮
    將GET_PAR中查找路徑上的節點直接指向根

    GET_PAR(a)
    1 2 3 4 //new code if (par[a] != a) ??Par[a]=GET_PAR(par[a]); return par[a];
    1 2 3 4 5 //old code if (par[a] == a) ??retrun a; else? ??return GET_PAR(par[a]);

    在解決方案二存在的情況下,解決方案一失去了優化效果!

    完整代碼:

    獲取根節點
    1 2 3 4 5 int get_par(int u) { ????if(par[a] != a) ????????par[a] = get_par(par[a]); ????return par[a]; }
    判斷兩個元素是否在一個集合中
    1 2 3 int query(int a, int b) { ????Return get_par(a) == get_par(b); }
    合并兩個集合
    1 2 3 void merge(int a, int b) { ????par[get_par(a)] = get_par(b); }

    應用篇

    POJ1611 The Suspects

    n個學生分屬m個團體,(0 < n <= 30000 ,0 <= m <= 500) 一個學生可以屬于多個團體。一個學生疑似患病,則它所屬的整個團體都疑似患病。已知0號學生疑似患病,以及每個團體都由哪些學生構成,求一共多少個學生疑似患病。

    解法:最基礎的并查集,把所有可疑的都并一塊。
    Sample Input
    100 4
    2 1 2
    5 10 13 11 12 14
    2 0 1
    2 99 2
    200 2
    1 5
    5 1 2 3 4 5
    1 0
    0 0
    Sample Output
    n4
    n1
    n1

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <iostream> #include <cstdio> using namespace std; const int MAX = 30000; int n,m,k; int parent[MAX+10]; int total[MAX+10]; //total[GetParent(a)]是a所在的group的人數 int GetParent(int a) { ????//獲取a的根,并把a的父節點改為根 ??? if( parent[a]!= a) ????????parent[a] = ????????????GetParent(parent[a]); ????return parent[a]; } void Merge(int a,int b) { ????int p1 = GetParent(a); ????int p2 = GetParent(b); ????if( p1 == p2 ) ????????return; ????total[p1] += total[p2]; ????parent[p2] = p1; } int main() { ????while(true) { ????????scanf("%d%d",&n,&m); ????????if( n == 0 && m == 0)break; ????????for(int i= 0; i < n; ++i) { ????????????parent[i] = i; ????????????total[i] = 1; ????????} ????????for(int i= 0; i < m; ++i) { ????????????int h,s; ????????????scanf("%d",&k); ????????????scanf("%d",&h); ????????????for( int j = 1; j < k; ++j) { ????????????????scanf("%d",&s); ????????????????Merge(h,s); ????????????} ????????} ????????printf("%d\n",total[GetParent(0)]); ????} ????return 0; }

    POJ 1988 Cube stacking
    有N(N<=30,000)堆方塊,開始每堆都是一個方塊。方塊編號1 –N. 有兩種操作:
    M x y :表示把方塊x所在的堆,拿起來疊放到y所在的堆上。
    C x : 問方塊x下面有多少個方塊。
    操作最多有P (P<=100,000)次。對每次C操作,輸出結果。

    解法:
    除了parent數組,還要開設
    sum數組:記錄每堆一共有多少方塊。
    若parent[a] = a, 則sum[a]表示a所在的堆的方塊數目。
    under數組,under[i]表示第i個方塊下面有多少個方塊。
    under數組在堆合并和路徑壓縮的時候都要更新。

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> #include <cstdio> using namespace std; const int MAX = 31000; int parent[MAX]; int sum[MAX]; // 若parent[i]=i,sum[i]表示磚塊i所在堆的磚塊數目 int under[MAX]; // under[i]表示磚塊i下面有多少磚塊 int GetParent(int a) { ????//獲取a的根,并把a的父節點改為根 ??? if( parent[a] == a) ????????return a; ????int t = GetParent(parent[a]); ????under[a] += under[parent[a]]; ????parent[a] = t; ????return parent[a]; } void Merge(int a,int b) { //把b所在的堆,疊放到a所在的堆。 ??? int n; ????int pa = GetParent(a); ????int pb= GetParent(b); ????if( pa == pb)return ; ????parent[pb] = pa; ????under[pb] = sum[pa]; //under[pb] 賦值前一定是0,因為parent[pb] = pb,pb一定是原b所在堆最底下的 ??? sum[pa] += sum[pb]; } int main() { ????int p; ????for(int i= 0; i< MAX; ++ i) { ????????sum[i] = 1; ????????under[i] = 0; ????????parent[i] = i; ????} ????scanf("%d",&p); ????for( int i= 0; i < p; ++ i) { ????????char s[20]; ????????int a,b; ????????scanf("%s",s); ????????if( s[0] == 'M') { ????????????scanf("%d%d",&a,&b); ????????????Merge(b,a); ????????} else { ????????????scanf("%d",&a); ????????????GetParent(a); ????????????printf("%d\n",under[a]); ????????} ????} ????return 0; }

    POJ 1182 食物鏈
    三類動物A、B、C,A吃B,B吃C,C吃A。
    給出K句話來描述N個動物(各屬于A、B、C三類之一)之間的關系,格式及意義如下:
    1 X Y:表示X與Y是同類;
    2 X Y:表示X吃Y。
    K句話中有真話有假話,當一句話滿足下列三條之一時,這句話就是假話,否則就是真話。1)當前的話與前面的某些真的話沖突,就是假話;2)當前的話中X或Y比N大,就是假話;3)當前的話表示X吃X,就是假話。
    求假話的總數。
    輸入:
    第一行是兩個整數N和K,以一個空格分隔。以下K行每行是三個正整數D,X,Y,兩數之間用一個空格隔開,其中D表示說法的種類。若D=1,則表示X和Y是同類。若D=2,則表示X吃Y。
    輸出:
    只有一個整數,表示假話的數目。
    約束條件:
    1 <= N <= 50000,0 <= K <= 100000。

    一個容易想到的思路:
    用二維數組s存放已知關系:
    S[X][Y] = -1:表示X與Y關系未知;
    S[X][Y] = 0:表示X與Y是同類;
    S[X][Y] = 1:表示X吃Y;
    S[X][Y] = 2:表示Y吃X。
    對每個讀入的關系s(x,y),檢查S[x][y]:
    若S[x][y]=s,則繼續處理下一條;
    若S[x][y] = -1,則令S[x][y]=s,并更新S[x][i]、S[i][x]、S[y][i]和S[i][y] (0<i<=n)。
    若S[x][y] != s且S[x][y] != -1,計數器加1。

    復雜度:
    以上算法需要存儲一個N×N的數組,空間復雜度為O(N2)。
    對每一條語句
    進行關系判定時間為O(1)
    加入關系時間為O(N)
    總的時間復雜度為O(N*K)
    0<=N<=50000,0<=K<=100000,復雜度太高。

    進一步分析
    對于任意a≠b,a、b屬于題中N個動物的集合S,當且僅當S中存在一個有限序列(P1, P2, …, Pm)(m≥0)使得aP1、P1P2、…、Pm-1Pm、Pmb(或m=0時的ab)之間的相對關系均已確定時,b對a的相對關系才可以確定。
    由上面可知,我們不需要保留每對個體之間的關系,只需要為每對已知關系的個體保留一條路徑aP1P2…Pmb(m≥0)其中aP1、P1P2、…、Pm-1Pm、Pmb之間的關系均為已知。兩兩關系已知的動物們,構成一個group

    解決方案
    使用并查集
    用結點表示每個動物,邊表示動物之間的關系。采用父結點表示法,在每個結點中存儲該結點與父結點之間的關系。
    parent數組:parent[i]表示i的父節點
    relation數組:relation[i]表示i和父節點的關系

    初始狀態下,每個結點單獨構成一棵樹。
    讀入a,b關系描述時的邏輯判斷:
    分別找到兩個結點a、b所在樹的根結點ra、rb,并在此過程中計算a與ra、b與rb之間的相對關系。
    若ra!=rb,此句為真話,將a、b之間的關系加入;
    若ra=rb,則可計算出r(a,b)=f( r(a,ra) , r(b,rb) )
    若讀入的關系與r(a,b)矛盾,則此句為假話,計數器加1;
    若讀入的關系與r(a,b)一致,則此句為真話。

    一些練習
    POJ 2492 A Bug?s Life
    法一:深度優先遍歷
    每次遍歷記錄下該點是男還是女,
    只有: 男->女,女->男滿足,
    否則,找到同性戀二分圖匹配,結束程序
    法二:并查集
    POJ 2524 最基礎的并查集
    POJ 1182 并查集的拓展有三類動物A,B,C,這三類動物的食物鏈構成了有趣的環形。A吃B,B吃C,C吃A。也就是說:只有三個group
    POJ 1861并查集+自定義排序+貪心求“最小生成樹”
    POJ 1703并查集的拓展
    POJ 2236 并查集的應用需要注意的地方:1、并查集;2、N的范圍,可以等于1001;3、從N+1行開始,第一個輸入的可以是字符串。
    POJ 2560最小生成樹
    法一:Prim算法;法二:并查集實現Kruskar算法求最小生成樹
    POJ 1456 帶限制的作業排序問題(貪心+并查集)

    總結

    以上是生活随笔為你收集整理的并查集入门的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。