傻子都能看懂的並查集入門

其實並查集顧名思義就是有「合併集合」和「查找集合中的元素」兩種操做的關於數據結構的一種算法。node

概述

性質

並查集算法不支持分割一個集合。ios

算法

用集合中的某個元素來表明這個集合,該元素稱爲集合的表明元
一個集合內的全部元素組織成以表明元爲根的樹形結構。
對於每個元素 parent[x]指向x在樹形結構上的父親節點。若是x是根節點,則令parent[x] = x。
對於查找操做,假設須要肯定x所在的的集合,也就是肯定集合的表明元。能夠沿着parent[x]不斷在樹形結構中向上移動,直到到達根節點。算法

判斷兩個元素是否屬於同一集合,只須要看他們的表明元是否相同便可。

路徑壓縮

爲了加快查找速度,查找時將x到根節點路徑上的全部點的parent設爲根節點,該優化方法稱爲壓縮路徑。
 
 使用該優化後,平均複雜度可視爲Ackerman函數的反函數,實際應用中可粗略認爲其是一個常數。

用途

一、維護無向圖的連通性。支持判斷兩個點是否在同一連通塊內,和。
二、判斷增長一條邊是否會產生環:用在求解最小生成樹的Kruskal算法裏。數組

reference

《ACM國際大學生程序設計競賽 知識與入門 俞勇主編》數據結構

三個操做

通常來講,一個並查集一三個操做。函數

初始化

包括對全部單個的數據創建一個單獨的集合(即根據題目的意思本身創建的最多可能有的集合,爲下面的合併查找操做提供操做對象)
在每個單個的集合裏面,有三個東西。
1,集合所表明的數據。(這個初始值根據須要本身定義,不固定)
2,這個集合的層次一般用rank表示(通常來講,初始化的工做之一就是將每個集合裏的rank置爲0)。
3,這個集合的類別parent(有的人也喜歡用set表示)(其實就是一個指針,用來指示這個集合屬於那一類,合併事後的集合,他們的parent指向的最終值必定是相同的。)
(**有的簡單題裏面集合的數據就是這個集合的標號,也就是說只包含2和3,1省略了)。
初始化的時候,一個集合的parent都是這個集合本身的標號。沒有跟它同類的集合,那麼這個集合的源頭只能是本身了。
(最簡單的集合就只含有這三個東西了,固然,複雜的集合就是把3指針這一項添加內容,如PKU食物鏈那題,咱們還能夠添加enemy指針,表示這個物種集合的天敵集合;food指針,表示這個物種集合的食物集合。隨着指針的增長,並查集操做起來也變得複雜,題目也就顯得更難了)優化

結構體表示法

有的人是創建一個結構體把集合表示出來,如:spa

#define MAX 10000
struct Node
{
    int data;
    int rank;
    int parent;
 }node[MAX];

數組表示法

有的人則是弄不少相同大小的數組,如:設計

int set[max];//集合index的類別,或者用parent表示
int rank[max];//集合index的層次,一般初始化爲0
int data[max];//集合index的數據類型

//初始化集合
void Make_Set(int i)
{
    set[i]=i;//初始化的時候,一個集合的parent都是這個集合本身的標號。沒有跟它同類的集合,那麼這個集合的源頭只能是本身了。
    rank[i]=0;
}
通常來講,題目簡單用數組,題目複雜用結構體,由於結構體有條理,數組能夠少打幾個字。

查找函數

就是找到parent指針的源頭,能夠把函數命名爲get_parent(或者find_set,這個隨你喜歡,以便於理解爲主)
若是集合的parent等於集合的編號(即尚未被合併或者沒有同類),那麼天然返回自身編號。
若是不一樣(即通過合併操做後指針指向了源頭(合併後選出的rank高的集合))那麼就能夠調用遞歸函數,以下面的代碼:指針

/**
*查找集合i(一個元素是一個集合)的源頭(遞歸實現)。
 若是集合i的父親是本身,說明本身就是源頭,返回本身的標號;
 不然查找集合i的父親的源頭。
**/
int get_parent(int x)
{
    if(node[x].parent==x)
        return x;
    return get_parent(node[x].parent);
}

數組的話就是:

//查找集合i(一個元素是一個集合)的源頭(遞歸實現)
int Find_Set(int i)
{ 
    //若是集合i的父親是本身,說明本身就是源頭,返回本身的標號
   if(set[i]==i)
       return set[i];
    //不然查找集合i的父親的源頭
    return  Find_Set(set[i]);        
}
int unifind(int a){// find the root and compress the path
    
    int root = a;
    
    //find the root
    while(root != parent[root] ){ // The parent of root is root itself.
        root = parent[root];
    }
    
    // compress the path
    while( a != root){
        int parentOfA = parent[a];
        parent[a] = root; // 將當前節點的父節點直接設置爲父節點
        a = parentOfA;
    }
    
    return root;
}

合併集合函數

clipboard.png

這就是所謂並查集的並了。至於怎麼知道兩個集合是能夠合併的,那就是題目的條件了。
先看代碼:

void Union(int a,int b)
{
    a=get_parent(a);
    b=get_parent(b);
    if(node[a].rank>node[b].rank)
        node[b].parent=a;
    else
    {    
        node[a].parent=b;
        if(node[a].rank==node[b].rank)
            node[b].rank++;
    }
}

再給出數組顯示的合併函數:

void Union(int i,int j)
{
    i=Find_Set(i);
    j=Find_Set(j);
    if(i==j) return ;
    if(rank[i]>rank[j]) set[j]=i;
    else
    {
        if(rank[i]==rank[j]) rank[j]++;   
        set[i]=j;
    }
}

計算最後有多少個不相交的集合

就算須要多少條邊能夠成爲連通圖

int count = 0; // the number of independent sets

即計算有多少個 parent[i] == i;

例題

華爲OJ·計算須要多少條邊能聯通

![圖片上傳中...]

AC代碼:

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2012-2018. All rights reserved.
 * Description: 項目 City Road 的源文件
 * Author: c00518290
 * Create: 2019-08-05
 */
#include <stdio.h>
#include <iostream>

using namespace std;

int parent[1002];

int main(){
    int n,m,a,b;
    int count=0;
    scanf("%d %d",&n,&m);
    int list[n+1];
    for(int i=1;i<=n;i++){
        list[i]=i;
    }
    for(int i=0;i<m;i++){
        scanf("%d %d",&a,&b);
        while(list[a]!=a){
            a=list[a];
        }
        while(list[b]!=b){
            b=list[b];
        }
        if(list[b]!=list[a]){
            list[b]=list[a];
        }
    }
    for(int i=1;i<=n;i++){
        if(list[i]==i)
            count++;
    }
    printf("%d\n",count-1);
}


/**
5 2
1 2
3 5
**/

華爲OJ·判斷是否聯通圖

![圖片上傳中...]

相關文章
相關標籤/搜索