【詳解】並查集高級技巧:加權並查集、擴展域並查集


一、普通並查集

  可以理解為使用數組實現的樹形結構,只保存了每個節點的父節點(前驅)。

  功能為:合並兩個節點(及其所在集合) 、 查找節點所屬集合的代表節點(可以理解為根節點)。

原理及用法

以6個元素為例(編號0到5):把0單獨划分為一個集合;把1,2,3,4划分為一個集合;把5單獨划分為一個集合。

  1. 初始化  init()

  n個元素的並查集,只需要一個容量為n的數組f[n],值全部初始化為自己即可:for(int i=0;i<n;i++) f[i]=i;

  2. 查找節點所屬集合  Find(x)

  主要代碼:Find(x):  if(x == f[x]) return x;

              return Find(f[x]);

  但若只是簡單的這樣做,會出現上圖第三個圓中的情況,即查找某個節點時遞歸太多次。因此需要“路徑壓縮”,只需增加一步:

       Find(x):  if(x == f[x]) return x;

              return f[x] = Find(f[x]);

  3. 合並兩個節點(及其所在集合)  Union(x, y)

    Union(x,y):  int fx=Find(x), fy=Find(y);

           if(fx != fy) f [fx] = fy;  // 此處換為f [fy] = fx也行,道理相同,意義和效果其實也一樣。

  注意:一定是f [fx] = fy,而不是f [x] = y。只有把x和y的最終父節點(前驅)連接起來,所屬的兩個集合才算真正完全連通,整個邏輯也才能正確。

二、擴展域並查集

使用情景:

  n個點有m對關系,把n個節點放入兩個集合里,要求每對存在關系的兩個節點不能放在同一個集合。問能否成功完成?

思路:

  把每個節點擴展為兩個節點(一正一反),若a與b不能在一起(在同一個集合),則把a的正節點與b的反節點放一起,把b的正節點與a的反節點放一起,這樣就解決了a與b的沖突。若發現a的正節點與b的正節點已經在一起,那么說明之前的某對關系與(a,b)這對關系沖突,不可能同時滿足,即不能成功完成整個操作。

具體實現:

  1. 初始化  init()

  n個點,每個點擴展為兩個點(一正一反),則需要一個容量為2*n的數組f[n],值全部初始化為自己即可:for(int i=0;i<2*n;i++) f[i]=i;

  (注意初始編號,若編號為[1,n],則初始化應該為:for(int i=1;i<=2*n;i++) f[i]=i;)

      一個點x的正點編號為x,反點編號為x+n(這樣每個點的反點都是+n,規范、可讀性強、不重復、易於理解)。

  2.  Find(x)和Union(x, y)不需要修改,含義和實現不變。

  3. 解決問題的算法步驟

  1)初始化2*n個節點的初始父節點,即它本身。

  2)遍歷m對關系,對每對(a,b),先找到a和b的父節點,若相等則說明(a,b)的關系與之前的關系有沖突,不能同時解決,則得到結果:不能完成整個操作。

      否則執行:Union(a, b+n), Union(b, a+n).  (這時已經Find過了,直接執行f [fx] = fy這一句就等效與Union(x, y) )

  3)若m對關系都成功解決,則得到結果:能夠完成整個操作。

拓展:

  由於擴展域會直接使數組容量翻倍,所有一般只解決這種“二分”問題,只擴展為2倍即可。

  優點在於:結構簡單,並查集的操作也不需要做改變,非常易於理解。  缺點顯然就是:需要額外存儲空間。

三、加權並查集

使用情景:

  N個節點有M對關系(M條邊),每對關系(每條邊)都有一個權值w,可以表示距離或划分成多個集合時的集合編號,問題依然是判斷是否有沖突或者有多少條邊是假的(沖突)等。

思路:

  給N個節點虛擬一個公共的根節點,增加一個數組s[n]記錄每個節點到虛擬根節點的距離,把x,y直接的權值w看為(x,y)的相對距離。

  Union(x,y,w)時額外把x,y到虛擬根節點的距離(s值)的相對差值設置為w;Find(x)時,壓縮路徑的同時把當前s值加上之前父節點的s值,得到真實距離。

具體實現:

  1. 初始化  init()

  f[n]數組記錄節點的父節點,s[n]數組記錄節點到虛擬根節點的距離:  for(int i=0;i<n;i++) {  f[i]=i;  s[i]=0; }

  2.  Find(x)

      if(x==f[x])return x;

      int t  = f[x];

      f[x] = Find(f[x]);

      s[x] += s[t];

      // s[[x] %= mod;  若s[x]表示划分成mod個集合時的集合編號等情況時,則需要求余。

      return f[x];

  3. Union(x, y,w)

      int fx = Find(x), fy = Find(y);  //此時已經s[x]和s[y]都已經計算為真值。

      if(fx != fy) {

        f [fx] = fy;

        s [fx] = (s[x] - s[y] + w + mod) % mod;

      }

  4. 解決問題的算法步驟

    初始化后,遍歷m對關系:若x,y的父節點不同,則Union(x,y,w);否則,若x與y的差值為w,則說明正確,繼續遍歷,不為w時說明出現沖突。

    當s[x]只是代表划分為mod個集合時的集合編號時,應該比較s[x]與s[y]的值是否相同,相同時說明出現沖突;不相同時說明之前已經解決了,正確可繼續遍歷。

拓展:加權並查集主要得賦予並理解s[x]值的意義,較難掌握且應用廣泛

  牛客網例題:關押罪犯 https://ac.nowcoder.com/acm/problem/16591 ,里面的題解和討論區有更多講解和入門題目鏈接

  直接百度搜素“加權並查集”也可找到更多講解和入門題目鏈接。

牛客網關押罪犯的題解代碼:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn = 20002;
const int maxm = 100002;

struct edge {
    int a, b, c;
}e[maxm];

bool cmp(edge a, edge b) {
    return a.c > b.c;
}

int f[2 * maxn];
int Find(int x) {
    if (x == f[x])return x;
    return f[x] = Find(f[x]);
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i++)
        scanf("%d%d%d", &e[i].a, &e[i].b, &e[i].c);
    sort(e, e + m, cmp);    //按仇恨值從大到小排序
    for (int i = 1; i <= 2 * n; i++)f[i] = i;    //初始化並查集

    int i;    //從大到小依次把每對罪犯安排到不同監獄
    for (i = 0; i < m; i++) {
        int a = Find(e[i].a), b = Find(e[i].b);
        if (a == b)break;    //兩人的正點已在同一個集合,無法解決,最大沖突出現
        f[a] = Find(e[i].b + n);    //把a和b的反點(敵人)合並
        f[b] = Find(e[i].a + n);    //把b和a的反點(敵人)合並(每個點都有一個正點和反點)
    }
    if (i == m)printf("0");
    else printf("%d", e[i].c);
    return 0;
}
擴展域的並查集解法
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 200000 + 10;
const int gxs = 2;    //模2就只有0,1兩個值,分別代表兩個不同的集合
const int mod = 2;
int n, m;
int f[maxn], s[maxn];    //f記錄父節點(前驅),s記錄到虛擬root點的距離

void init() {
    for (int i = 0; i < maxn; i++) f[i] = i, s[i] = 0;
}
//查找
int finds(int x) {
    if (x == f[x]) return x;
    int t = f[x];
    f[x] = finds(f[x]);
    s[x] += s[t];    //s[x]原來是與t的相對距離,現在是與root的相對距離
    s[x] %= gxs;    //s值求余后代表所屬監獄(二選一)
    return f[x];
}

//新建關系
void unions(int x, int y, int w) {
    int fx = finds(x), fy = finds(y);
    if (fx != fy) {
        f[fy] = fx;
        s[fy] = s[x] - s[y] + w + gxs;    //相對距離設置為w,解決這一對沖突
        s[fy] %= mod;        //求余直接賦予實際意義:所屬的mod個集合的編號
    }
}

struct node {
    int a, b;
    ll val;
    bool operator < (const node &a)const {
        return val > a.val;
    }
};

vector<node> que;

int main() {
    cin >> n >> m;
    int a, b;
    ll v;
    for (int i = 0; i < m; i++) {
        cin >> a >> b >> v;
        que.push_back(node{ a,b,v });
    }
    sort(que.begin(), que.end());    //從大到小排序
    init();
    for (int i = 0; i < m; i++) {
        a = que[i].a;
        b = que[i].b;
        v = que[i].val;
        if (finds(a) == finds(b)) {    //在同一個集合就不能直接解決沖突
            if (s[a] == s[b]) {            //若s值相同就說明已經在同一個集合,沖突無法解決
                cout << v << endl;        //因為從大到小遍歷,第一個解決不了的關系的val就是答案:最小化的最大沖突值
                return 0;
            }                            //否則說明解決之前的沖突后,當前沖突也被解決。
        }
        else {        //不在一個集合就可以通過設置s值解決沖突
            unions(a, b, 1);
        }
    }
    cout << 0 << endl;
    return 0;
}
加權並查集解法

 


免責聲明!

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



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