並查集的使用及其實現

The_best_man發表於2017-03-16

並查集

概述

詳細教程參考之前轉載的並查集詳解


性質

並查集演算法(union_find sets)不支援分割一個集合,求連通子圖、求最小生成樹

用法

並查集是由一個陣列pre[],和兩個函式構成的,一個函式為find()函式,用於尋找前導點的,第二個函式是join()用於合併路線的

int find(int x)
{
    int r=x;
    while(pre[r]!=r)
    r=pre[r];//找到他的前導結點
    int i=x,j;
    while(i!=r)//路徑壓縮演算法
    {
        j=pre[i];//記錄x的前導結點
        pre[i]=r;//將i的前導結點設定為r根節點
        i=j;
    }
    return r;
}

路徑壓縮為了加快查詢的速度,將x點與其根節點直接相連,構造成類似於只有葉子結點而沒有分支結點的樹


join()函式

void join(int x,int y)
{
    int a=find(x);//x的根節點為a
    int b=find(y);//y的根節點為b
    if(a!=b)//如果a,b不是相同的根節點,則說明ab不是連通的
    {
        pre[a]=b;//我們將ab相連 將a的前導結點設定為b
    }
}

初始化

我們將每一個結點的前導結點設定為自己,如果在join函式時未能形成連通,將獨立成點

for(int i=0;i<n;i++)//n表示輸入的結點的個數
{
    pre[i]=i;//將每一個結點的前導點設定為自己

}

用法

試題來自第八屆藍橋杯試題
第三次編輯這道題目
標題:風險度量

X星系的的防衛體系包含 n 個空間站。這 n 個空間站間有 m 條通訊鏈路,構成通訊網。
兩個空間站間可能直接通訊,也可能通過其它空間站中轉。

對於兩個站點x和y (x != y), 如果能找到一個站點z,使得:
當z被破壞後,x和y無法通訊,則稱z為關於x,y的關鍵站點。

顯然,對於給定的兩個站點,關於它們的關鍵點的個數越多,通訊風險越大。

你的任務是:已知網路結構,求兩站點之間的通訊風險度,即:它們之間的關鍵點的個數。

輸入資料第一行包含2個整數n(2 <= n <= 1000), m(0 <= m <= 2000),分別代表站點數,鏈路數。
空間站的編號從1到n。通訊鏈路用其兩端的站點編號表示。
接下來m行,每行兩個整數 u,v (1 <= u, v <= n; u != v)代表一條鏈路。
最後1行,兩個數u,v,代表被詢問通訊風險度的兩個站點。

輸出:一個整數,如果詢問的兩點不連通則輸出-1.

例如:
使用者輸入:
7 6
1 3
2 3
3 4
3 5
4 5
5 6
1 6
應該輸出:
2

我的錯誤

在進行分析的時候,我考慮了去邊去點,去點的話,首先逐個去掉除了詢問的點以外的點,同時去點的同時我們同樣需要去掉與該點之間關聯的邊,查詢與該點關聯的邊需要從整個資料中尋找資料量太大,一定會超時,還是需要考慮去邊的辦法,

//並查集
#include<iostream>
using namespace std;
int pre[1005];//每個點的前導點
int route[2005][2];
//可以配對的路線
int sum = 0;
//符合條件的 即關鍵點的數量

//查詢
int find(int x)
{
    int r = x;
    while (pre[r] != r)
        r = pre[r];
    int i = x, j;
    while (i != r)//路徑壓縮演算法
    {
        j = pre[i];//在改變他的前導點時,儲存他的值
        pre[i] = r;
        i = j;//改變他的前導點為根節點
    }
    return r;
}


void join(int x, int y)
//組合
{
    int fx = find(x), fy = find(y);//分別記錄x,y的根節點
    if (fx != fy)//如果他們的根節點相同,則說明他們不是連通圖
        pre[fx] = fy;//將x的根結點 同 相連線
}

int main()
{
    int n, m;
    cin >> n>>m;//n表示站點的個數,m表示鏈路的個數

    for (int i = 0; i < m; i++)
    {
        cin >> route[i][0] >> route[i][1];
        join(route[i][0], route[i][1]);//將資料相互連線
    }


    int q1,q2;//待詢問的兩個點
    cin >> q1 >> q2;


    for (int ii = 0; ii < n; ii++)pre[ii] = ii;
    for (int j = 0; j < m; j++)
    {

            join(route[j][0], route[j][1]);
    }
    int a = find(q1);
    int b = find(q2);
    //如果邊全部存在時不可達,則輸出 -1;
    if (a != b)
    {
        cout << "-1" << endl;
    }

    else
    {
        for (int i = 1; i <= n; i++)
//列舉每一個點
        {
            if (i == q1 || i == q2)continue;
//如果是被詢問的點,跳過,無需遍歷   此處是最關鍵的部分
            for (int j = 1; j <= n; j++)pre[j] = j;
//將每一個初始化

            for (int j = 0; j < m; j++)
            {
                if (route[j][0] == i || route[j][1]==i)continue;
//去除當前點互相關聯的邊   解決問題的關鍵
                int a = find(route[j][0]);
                int b = find(route[j][1]);
                if (a > b) { a ^= b; b ^= a; a ^= b; };//交換
                if (a != b)pre[b] = a;
//以較小的點作為父節點
            }
            int a = find(q1);
            int b = find(q2);
            if (a != b)sum++;
            }
            cout<<sum<<endl;
        }
        return 0;
}

看到網上好多人在寫並查集時,使用while(~sacnf("%d",&a)) scanf()函式的返回值是正確獲得變數的個數
~scanf()函式就是沒有得到正確的輸入,總體上講如果有正確結果輸入,就退出迴圈,如果沒有正確輸入,就執行迴圈
看似沒有什麼區別,其實這種while()迴圈更加安全,保證不會因為非法的數字的輸入執行程式的使用


測試資料

按照我之前對於資料的統計,現在給大家提供幾組資料

  1. 4 0
    1 2
    測試結果 -1
    1. 4 3
      2 3
      3 4
      2 4
      1 4
      測試結果 -1
    2. 3 2
      1 2
      2 3
      1 3
      測試結果 1
    3. 使用題目的資料 以及評論區的那個資料

測試資料的分析,對於第一組測試資料,有且僅有四個點 ,測試程式是否會進行連通性判斷;第二組資料 1 是獨立點 234是三個連通分量,程式需要判斷是否1與其他的點能構成連通圖;第三組資料,我們測試一條直線,所有的關鍵點全部在該直線上,判斷程式記錄的到底是關鍵點的個數還是邊的個數;第四組我們測試任意情況下對於資料的處理我們可以開啟畫板,對我們的資料進行驗證即可


反向並查集

題目


來自藍橋杯系統歷屆試題庫中的試題

問題描述
  C國由n個小島組成,為了方便小島之間聯絡,C國在小島間建立了m座大橋,每座大橋連線兩座小島。兩個小島間可能存在多座橋連線。然而,由於海水沖刷,有一些大橋面臨著不能使用的危險。

  如果兩個小島間的所有大橋都不能使用,則這兩座小島就不能直接到達了。然而,只要這兩座小島的居民能通過其他的橋或者其他的小島互相到達,他們就會安然無事。但是,如果前一天兩個小島之間還有方法可以到達,後一天卻不能到達了,居民們就會一起抗議。

  現在C國的國王已經知道了每座橋能使用的天數,超過這個天數就不能使用了。現在他想知道居民們會有多少天進行抗議。
輸入格式
  輸入的第一行包含兩個整數n, m,分別表示小島的個數和橋的數量。
  接下來m行,每行三個整數a, b, t,分別表示該座橋連線a號和b號兩個小島,能使用t天。小島的編號從1開始遞增。
輸出格式
  輸出一個整數,表示居民們會抗議的天數。
樣例輸入
4 4
1 2 2
1 3 2
2 3 1
3 4 3
樣例輸出
2
樣例說明
  第一天後2和3之間的橋不能使用,不影響。
  第二天後1和2之間,以及1和3之間的橋不能使用,居民們會抗議。
  第三天後3和4之間的橋不能使用,居民們會抗議。
資料規模和約定
  對於30%的資料,1<=n<=20,1<=m<=100;
  對於50%的資料,1<=n<=500,1<=m<=10000;
  對於100%的0<=n<=10000,1<=m<=10000。
  ,1<=a, b<=n, 1<=t<=100000。

#include<iostream>
#include<algorithm>
using namespace std;
struct node
{
    int x, y, d;//d表示剩餘的時間  x,y分別表示橋的兩端的端點
}bridge[10005];//建立橋的數量
int pre[10005];//前導結點的個數

bool cmp(node a, node b)//時間比較   如果第一個引數大於第二個引數,則返回true
{
    return a.d>b.d;
}

int find(int x)//查詢根節點
{
    int r=x;
    while (pre[r] != r)
        r = pre[r];

    //嘗試嘗試路徑壓縮演算法

    return r;
}
bool join(int x, int y)//合併鏈路
{
    int fx = find(x);
    int fy = find(y);
    if (fx != fy)
    {
        pre[fx] = fy;
        return 1;//沒有橋,我們將直接構造形成橋
    }
    return 0;//有橋無需構造
}
int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < m; i++)
    {
        cin >> bridge[i].x >> bridge[i].y >> bridge[i].d;//輸入橋頭兩側 使用天數
    }
    //
    for (int i = 0; i < n; i++)
    {
        pre[i] = i;//初始化每個小島,使其獨立
    }


    //按照使用時間排序進行整合
    sort(bridge, bridge + m, cmp);//天數從大到小排列
    int fight = 0;//表示反抗的日子
    int pre = -1;//
    for (int i = 0; i < m; i++)//此時的時間已經是從大到小的排序
    {
        int way = join(bridge[i].x, bridge[i].y);//從時間從大到小重新構造橋
        if (way == 1 && bridge[i].d != pre)//如果系統構造的橋並且天數不等於-1
        {
            fight++;
            pre = bridge[i].d;
        }
    }
    cout << fight << endl;

    return 0;
}

這是我的程式碼在通過系統的時候由於超出限制時間,只有40%的分數,其中由於一次需要輸入三個變數,並且我們在之後的操作中需要對時間進行排序,所以我們採取結構體命名變數我們建時間按照從大到小的順序排列,將每個島嶼全部獨立分開重新構建來連通圖,我們將按照時間天數優先,針對測試樣例,以及之前的順序優先準則來重新構建,從第一條邊開始,如果不是連通圖,就呼叫一次fight++,直到所有的結點全部構成連通圖,結束執行,無論後面還有多少條未加入的邊


未完待續

相關文章