小白學並查集(帶製作)

Powered by:AB_IN 局外人

  • 首先什麼是並查集?

並查集,在一些有N個元素的集合應用問題中,我們通常是在開始時讓每個元素構成一個單元素的集合,然後按一定順序將屬於同一組的元素所在的集合合併,其間要反覆查找一個元素在哪個集合中。這一類問題近幾年來反覆出現在信息學的國際國內賽題中,其特點是看似並不複雜,但數據量極大,若用正常的數據結構來描述的話,往往在空間上過大,計算機無法承受;即使在空間上勉強通過,運行的時間複雜度也極高,根本就不可能在比賽規定的運行時間(1~3秒)內計算出試題需要的結果,只能用並查集來描述。
(摘自百度)

  • 是不是覺得很難很抽象?菜雞一開始也這麼覺得,其實看看標程,自己琢磨一下就懂了。
    例題來自 寧波工程學院2020新生校賽(重現賽) L 小梁的道館
    • 題目描述:
      小梁變強之後決定建設自己的道館,她特別喜歡去其他的道館串門。但是有些道館之間沒有道路連通,於是小梁想知道自己能不能去她想去的 道館。你能幫她寫一個程序來查詢兩個道館之間是否互相存在道路聯通嗎?如果存在輸出“YES”,反之輸出“NO”。

    • 輸入描述:
      第一行爲三個整數N爲道館個數,M爲線路條數,T爲查詢次數。(1N<1000,1M<1000,1T<10000)(1 \leq N<1000,1 \leq M<1000, 1 \leq T<10000)

      第二行至第M+1行,每行兩個整數,代表兩個道館的編號a,b(1a1000,1b1000)a,b(1 \leq a \leq 1000,1 \leq b \leq 1000),表示這兩個道館之間有道路相連。

      第M+2行至第M+T+2M \text{+} T+2行,每行兩個整數,代表查詢這兩個道館。

    • 輸出描述:
      T行,每行對應一個查詢,假如查詢的道館之間可以連接則輸出YES,否則則輸出NO。

  1. 好,我們現在來看這道題。道館可以比作一個個點,一共有N個點。然後給出M條線路,再讓你查詢T次。
  2. 一開始我怎麼想的呢,我想把每次輸入的數變成一個列表,排個序,存在另一個大列表裏,然後查找時,看看輸入在不在大列表裏。(python實現)
  3. 但其實這麼想是錯誤的。因爲如果輸入的是
    1 21 \ 2
    2 32 \ 3
    那麼就不知道1和3其實也連起來了,只知道1和2連起來,2和3連起來了。
    所以這該怎麼實現啊?
  4. 爲了方便起見,我們舉個栗子:
    假設 N=6 然後M=5,線路條數分別是:
3 1
2 1
4 6
5 6
1 4

在這裏插入圖片描述
ok,是這個樣子的。這個其實對應了第一步(預處理),就是將數組的每個數的值都賦上下標的值。(就是圈圈裏填上數字)
好,接下來我們執行我們的3 1操作。
在這裏插入圖片描述
可以看到我故意 讓第一個數指向了第二個數,好,我們暫且讓11成爲父節點33成爲子節點,可以理解爲
>(先輸入)子節點—>(後輸入)父節點
好,我們繼續
在這裏插入圖片描述
OK,可以看到22 也是 11的小弟(子節點)了。
接下來就是445566做大哥(父節點)了。
在這裏插入圖片描述
到了比較關鍵步驟了 2>42—>4
在這裏插入圖片描述
好了,圖到這裏畫完了 。看這張圖,我們知道 2 42\ 4是相連的,但是,其實1 61\ 63 53\ 5 都是相連的啊,看這張圖的箭頭我走不過去啊!!
到了關鍵步驟了!!:核心思想:找老大(父節點)

  • 2 42\ 4在外面可都是小弟啊!怎麼辦?回去找老大唄!

  • 22的老大是1144的老大是662>42—>4 說明 22 的老大臣服於 44 的老大了,所以其實就相當於1>61—>6

  • 所以真正的圖是這樣的:
    在這裏插入圖片描述
    解讀一下核心思想:找老大(父節點),我們就可以做題啦!

    其實判斷兩個數是否相連,其實就是看他們的父節點是否一致
    舉個栗子:判斷3 43\ 4 是否連通?
    先看3333到父節點的路徑爲
    3>1>63—>1—>6
    再看4444到父節點的路徑爲
    4>64—>6
    不難看出,他們兩個的父節點相同,所以他倆連通。


呱唧呱唧
思想我們明白了,那麼如何代碼實現呢?

  • 首先我們定義個數組
fa[1001]
  • 預處理(前面說過的)
 for(int i = 1;i <= n; i++) fa[i] = i;
  • 接下來輸入兩個數
scanf("%d %d", &a, &b);
  • 接下來讓這兩個小弟找老大!
    這裏就牽扯到了find函數
int find(int x) {
    if(fa[x] == x) return x;
    return fa[x] = find(fa[x]);
}

找老大的過程像是遞歸,沒錯,就是遞歸思想實現!

  • 如果還是自己本身的值,有兩種可能:
  1. 個人練習生 (沒有老大,之前還沒被利用)。
  2. 是最終boss(目前的父節點,如上面例子的最終圖中的66
  • 不是自己本身的值:

    說明已經在連線中了,用遞歸不斷調用find函數,來找到父節點,如果遞歸中碰到了那個 函數值和下標相等的點,那就是父節點,跳出即可。

  • 可能要問了 ,怎麼遞歸啊,怎麼賦值啊?
    找到兩個的父節點,讓fa[左父節點]=右父節點即可,有點數組鏈表的意思。

	u = find(a); v = find(b);
	fa[u] = v;

如果暈乎的話,菜雞再舉個栗子:
比如 上面我舉的栗子中 :

  1. 輸入 3 13\ 133find一找發現 33就是 33的老大, 11find一找發現 1111的老大。
    所以fa[3]=1
  2. 輸入 2 42\ 4 :我們從前面知道fa[2]=1,開始找22的父節點,先發現fa[2]!=2,執行else,開始找fa[2](也就是1)的父節點,發現fa[1]=1,那麼就return 1回到上一層,fa[2]=1,那麼就最終return 144同理。
  • 最終一步,輸入T,有T次查找,前面說了,看兩個數是否連通,就是看兩個數父節點是否相同。所以:
scanf("%d %d", &a, &b);
u = find(a); v = find(b);
if(u == v) puts("YES");
else puts("NO");

好啦,介紹完成!
完整代碼如下,可以邊看代碼邊debug縷一縷思路哦!

#include <bits/stdc++.h>
using namespace std;
int n, m, t, a, b, u, v;
int fa[1005];

int find(int x) {
    if(fa[x] == x) return x;
    return fa[x] = find(fa[x]);
}

int main() {
    scanf("%d %d %d", &n, &m, &t);
    for(int i = 1;i <= n; i++) fa[i] = i;
    for(int i = 1;i <= m; i++) {
        scanf("%d %d", &a, &b);
        u = find(a); v = find(b);
        fa[u] = v;
    }
    while(t--) {
        scanf("%d %d", &a, &b);
        u = find(a); v = find(b);
        if(u == v) puts("YES");
        else puts("NO");
    }
}

完結。
困死了~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章