看正月點燈籠老師的筆記— 並查集


視頻地址  :https://www.bilibili.com/video/av38498175?p=1

 參考鏈接:借這個問題科普一下並查集各種情況下的時間復雜度 - 省份數量 - 力扣(LeetCode) (leetcode-cn.com)

 

 

一,並查集(Disjoint Set)概述

1,並查集的作用

① 檢查圖中是否存在環

2 ,並查集的流程

① 設定一個集合,叫並查集

② 往集合里面添加邊,怎么添加呢?取邊的起點和終點,判斷兩點是否都在集合里面。如果都在,則出現了環,如果不在,則將兩個點放入集合中。

③ 繼續添加下一條邊,直到沒有邊。如果最后都沒有找到環,就是圖中不存在環。

 

 

二,並查集的構造

1, 在上述並查集的流程中,如果我們用集合表示並查集,自然也可以實現。但使用集合的話,在進行“集合合並”或者是“點是否屬於該集合的判斷”的話,時間復雜度應該是高於使用根數組(憑感覺,關於時間復雜度真的搞不懂)。

2, 並查集構造的三個動機:

  能夠表示點加入集合的不同狀態;方便查找點是否存在於集合中;方便兩個不同的集合進行合並。

3, 根數組(我不知道專業名稱,這里暫時這樣稱呼)

為了滿足上述三點,於是便有人想出了並查集算法,想出了用根數組:p 實現並查集。

p[i]:表示第i個點的父節點。

p 的初始化:p[i] = i; 或 p[i] = -1;

4, 表示點加入集合的不同狀態

根數組用樹的結構去表示點的狀態。為什么用樹呢?因為並查集算法就是為了檢查環的存在,所以一旦有環的存在就會被判定為異常,即並查集無需表示環,而無環的連通圖就是樹。

有了數組p[i],就能根據父節點構造出森林出來,位於同一棵樹的點自然屬於同一集合。

5, 查找點是否存在於集合

並查集算法用根代表某一個集合。如果兩個點的根一樣,則表明兩個點處於同一棵樹上,即兩個點同處於它們的根所代表的集合中。

而查找根的方法我們可以輕易根據數組p實現,只需要一層一層的用父節點往上循環,直到根節點。

那么,如何判斷是否為根節點呢?因為根節點從未加入其他節點,所以根據初始化條件的不同,根節點的 p[i] = i; 或 p[i] = -1; 這就是初始化的目的。

6, 集合的合並

既然,我們根據樹和根節點來作為集合的判斷依據,那么,如果我們要合並集合a和集合b,其實就是合並樹a和樹b。所以我們只需要將樹a的根指向樹b的根,或者將樹b的根指向樹a就可以了。

7, 由於在合並集合的時候,我們對邊的順序是沒有要求的。這種連接方式,並不能正確表示原來圖的結構,只能表示點的連通關系。

 

 8, 代碼

#define _CR_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 10
int p[N];
int find(int x) // 找到根節點
{
    int t = x;
    while (p[t] != -1)
        t = p[t];
    return t;
}
int join(int x, int y) // 合並兩個集合
{
    x = find(x), y = find(y);
    if (x == y)
        return 0;
    p[x] = y;
    return 1;
}
int main(void)
{
    memset(p, -1, sizeof(p)); // 初始化
    int edges[7][2] = {          // 邊集
        {0,1},{1,2},{2,3},{4,5},{5,6},{1,4},{1,6}
    };

    int f = 0;
    for (int i = 0; i < 7; i++)
    {
        int x = edges[i][0];
        int y = edges[i][1];
        if (join(x, y) == 0)
        {
            printf("存在環!\n");
            f = 1;
            break;
        }
    }
    if (f == 0)
        printf("不存在環!\n");

    system("pause");
    return 0;
}
View Code

 

 

三,按秩合並 與 路徑壓縮

1,目的:對上述算法中:“查找點的根”,這一步驟的時間復雜度的優化。

2,舉例說明:在集合合並的時候,在極端情況下會出現 0-1 1-2 2-3 3-4…… 這樣一直讓樹的深度增加的情況。

這種情況就會導致點在查找根的時候,時間復雜度的增加。

3,所以,為了降低算法的時間復雜度,有人提出了壓縮路徑和按秩合並的思想。

4,按秩合並

① 秩:這里指樹的深度。算法使用 rank 數組來記錄樹的深度,如 rank[x] = y 表示 以 x 點為根結點的樹的深度為 y。

② 算法未開始時,此時所有的樹只有一個點,沒有邊,所以每個點的深度為 0,所以rank數組初始化為全0

③ 算法開始合並時,比較要合並的兩棵樹的深度。

當兩棵樹的深度不一致時,讓低的樹的根指向高的樹的根,這樣新合並的樹的高度就等於之前高的樹的深度,而不會再度增加。

當兩棵樹的深度一致時,隨便讓一棵樹的根指向另一棵樹的根,這樣新合並的樹的高度就等於之前樹的深度加上1,而不會增加很多。

④ 代碼

#define _CR_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 10
int p[N], rank[N];
int find(int x) // 找到根節點
{
    int r = x;
    while (p[r] != -1)
        r = p[r];
    return r;
}
int join(int x, int y) // 合並兩個集合
{
    x = find(x), y = find(y);
    if (x == y)
        return 0;
    if (rank[x] > rank[y]) // 讓低的指向高的
        p[y] = x;
    else if (rank[x] < rank[y])
        p[x] = y;
    else
    {
        p[x] = y;
        rank[y]++;
    }
    return 1;
}
int main(void)
{
    memset(p, -1, sizeof(p)); // 初始化
    memset(rank, 0, sizeof(rank));
    int edges[7][2] = {          // 邊集
        { 0,1 },{ 1,2 },{ 2,3 },{ 4,5 },{ 5,6 },{ 1,4 },{ 1,6 }
    };

    int f = 0;
    for (int i = 0; i < 7; i++)
    {
        int x = edges[i][0];
        int y = edges[i][1];
        if (join(x, y) == 0)
        {
            printf("存在環!\n");
            f = 1;
            break;
        }
    }
    if (f == 0)
        printf("不存在環!\n");

    system("pause");
    return 0;
}
View Code

5,壓縮路徑

① 直接在每一次查找某一點的根節點后,將該點到根節點的路徑上的所有點指向根節點。

② 不過這種壓縮路徑存在一定的延遲,即兩個集合剛合並后,你並沒有完成壓縮路徑,而是在查找時,才會去壓縮路徑。

正如圖中,剛開始並沒有用到除了根節點的點,所以一直沒有壓縮路徑。

一直到“取1-4”,此時點1和點4都不是根節點,所以在合並之前的查找,它會將點1到根節點路徑上的點指向它的根節點3,將點4到根節點路徑上的點指向它的根節點6。、

最后,壓縮完路徑的兩個樹在進行合並。

③ 代碼

#define _CR_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 10
int p[N];
int find(int x) // 找到根節點
{
    int r = x;
    while (p[r] != -1)
        r = p[r];
    while (x != r)
    {
        int t = p[x];
        p[x] = r;
        x = t;
    }
    return x;
}
int join(int x, int y) // 合並兩個集合
{
    x = find(x), y = find(y);
    if (x == y)
        return 0;
    p[x] = y;
    return 1;
}
int main(void)
{
    memset(p, -1, sizeof(p)); // 初始化
    int edges[7][2] = {          // 邊集
        {0,1},{1,2},{2,3},{4,5},{5,6},{1,4},{1,6}
    };

    int f = 0;
    for (int i = 0; i < 7; i++)
    {
        int x = edges[i][0];
        int y = edges[i][1];
        if (join(x, y) == 0)
        {
            printf("存在環!\n");
            f = 1;
            break;
        }
    }
    if (f == 0)
        printf("不存在環!\n");

    system("pause");
    return 0;
}
View Code
#define _CR_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 10
int p[N];
int find(int x)
{
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}
int join(int x, int y) // 合並兩個集合
{
    x = find(x), y = find(y);
    if (x == y)
        return 0;
    p[x] = y;
    return 1;
}
int main(void)
{
    for (int i = 0; i < N; i++) // 初始化
        p[i] = i;
    int edges[7][2] = {          // 邊集
        { 0,1 },{ 1,2 },{ 2,3 },{ 4,5 },{ 5,6 },{ 1,4 },{ 1,6 }
    };

    int f = 0;
    for (int i = 0; i < 7; i++)
    {
        int x = edges[i][0];
        int y = edges[i][1];
        if (join(x, y) == 0)
        {
            printf("存在環!\n");
            f = 1;
            break;
        }
    }
    if (f == 0)
        printf("不存在環!\n");

    system("pause");
    return 0;
}
View Code

 其中,第二個代碼是用遞歸實現 find函數,搜索時找根節點,回溯時壓縮路徑。

而且初始化為-1時,不能用遞歸實現,因為要指向根節點,如果初始化為自身則可以將返回值作為根節點,-1則不行。

 

 

四,按秩合並 + 路徑壓縮

① 只是簡單的將兩個函數放在一起,不同做什么特殊處理

② 明明壓縮路徑的時候,改變了樹高,那么,為什么rank數組不需要維護?

答:不太清楚。應該是相對高度不變吧。

③ 代碼

#define _CR_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 10
int p[N], rank[N];
int find(int x)
{
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}
int join(int x, int y) // 合並兩個集合
{
    x = find(x), y = find(y);
    if (x == y)
        return 0;
    if (rank[x] > rank[y]) // 讓低的指向高的
        p[y] = x;
    else if (rank[x] < rank[y])
        p[x] = y;
    else
    {
        p[x] = y;
        rank[y]++;
    }
    return 1;
}
int main(void)
{
    for (int i = 0; i < N; i++) // 初始化
        p[i] = i;
    memset(rank, 0, sizeof(rank));
    int edges[7][2] = {          // 邊集
        { 0,1 },{ 1,2 },{ 2,3 },{ 4,5 },{ 5,6 },{ 1,4 },{ 1,6 }
    };

    int f = 0;
    for (int i = 0; i < 7; i++)
    {
        int x = edges[i][0];
        int y = edges[i][1];
        if (join(x, y) == 0)
        {
            printf("存在環!\n");
            f = 1;
            break;
        }
    }
    if (f == 0)
        printf("不存在環!\n");

    system("pause");
    return 0;
}
View Code

 

 

 

=========== ========= ========= ======= ====== ====== ===== === == =

菩薩蠻 其三  唐 韋庄

如今卻憶江南樂,當時年少春衫薄。騎馬倚斜樓,滿樓紅袖招。

翠屏金屈曲,醉入花叢宿。此度見花枝,白頭誓不歸。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM