【轉】種類並查集

panfengblog發表於2020-11-09

【轉】種類並查集

拖了一大堆演算法 : ( , 行吧,寫不過來了~ 直接轉一篇大佬寫的叭~~

轉自這裡

並查集的應用很多,今天我們來看一個並查集的擴充——種類並查集

一般的並查集,維護的是具有連通性、傳遞性的關係,例如親戚的親戚是親戚。但是,有時候,我們要維護另一種關係:敵人的敵人是朋友。種類並查集就是為了解決這個問題而誕生的。

我們先來看一個例題:

洛谷P1525 關押罪犯

題目描述

S 城現有兩座監獄,一共關押著 N 名罪犯,編號分別為 1-N。他們之間的關係自然也極不和諧。很多罪犯之間甚至積怨已久,如果客觀條件具備則隨時可能爆發衝突。我們用“怨氣值”(一個正整數值)來表示某兩名罪犯之間的仇恨程度,怨氣值越大,則這兩名罪犯之間的積怨越多。如果兩名怨氣值為 c 的罪犯被關押在同一監獄,他們倆之間會發生摩擦,並造成影響力為 c 的衝突事件。

每年年末,警察局會將本年內監獄中的所有衝突事件按影響力從大到小排成一個列表,然後上報到 S 城 Z 市長那裡。公務繁忙的 Z 市長只會去看列表中的第一個事件的影響力,如果影響很壞,他就會考慮撤換警察局長。

在詳細考察了N 名罪犯間的矛盾關係後,警察局長覺得壓力巨大。他準備將罪犯們在兩座監獄內重新分配,以求產生的衝突事件影響力都較小,從而保住自己的烏紗帽。假設只要處於同一監獄內的某兩個罪犯間有仇恨,那麼他們一定會在每年的某個時候發生摩擦。

那麼,應如何分配罪犯,才能使 Z 市長看到的那個衝突事件的影響力最小?這個最小值是多少?

輸入格式

每行中兩個數之間用一個空格隔開。第一行為兩個正整數 N,M,分別表示罪犯的數目以及存在仇恨的罪犯對數。接下來的 MM 行每行為三個正整數 a j , b j , c j a_j,b_j,c_j aj,bj,cj,表示 a j a_j aj 號和 b j b_j bj 號罪犯之間存在仇恨,其怨氣值為 c j c_j cj。資料保證 1 < a j ≤ b j ≤ N 1<a_j\leq b_j\leq N 1<ajbjN, 0 < c j ≤ 1 0 9 0 < c_j\leq 10^9 0<cj109,且每對罪犯組合只出現一次。

輸出格式

共 1 行,為 Z 市長看到的那個衝突事件的影響力。如果本年內監獄中未發生任何衝突事件,請輸出 0。

其實很容易想到,這裡可以貪心,把所有矛盾關係從大到小排個序,然後儘可能地把矛盾大的犯人關到不同的監獄裡,直到不能這麼做為止。這看上去可以用並查集維護,但是有一個問題:我們得到的資訊,不是哪些人應該在相同的監獄,而是哪些人應該在不同的監獄。這怎麼處理呢?這個題其實有很多做法,但這裡,我們介紹使用種類並查集的做法。


我們開一個兩倍大小的並查集。例如,假如我們要維護4個元素的並查集,我們改為開8個單位的空間:

BbixL4.png

我們用14維護**朋友**關係(就這道題而言,是指關在同一個監獄的獄友),用58維護敵人關係(這道題裡是指關在不同監獄的仇人)。現在假如我們得到資訊:1和2是敵人,應該怎麼辦?

我們merge(1, 2+n), merge(1+n, 2);。這裡n就等於4,但我寫成n這樣更清晰。對於1個編號為i的元素,i+n是它的敵人。所以這裡的意思就是:1是2的敵人,2是1的敵人。

BbFSeJ.png

現在假如我們又知道2和4是敵人,我們merge(2, 4+n), merge(2+n, 4);

BbFPF1.png

發現了嗎,敵人的敵人就是朋友,2和4是敵人,2和1也是敵人,所以這裡,1和4通過2+n這個元素間接地連線起來了。這就是種類並查集工作的原理。

程式碼如下:

#include <cstdio>
#include <cctype>
#include <algorithm>
int read() //快速讀入,可忽略
{
    int ans = 0;
    char c = getchar();
    while (!isdigit(c))
        c = getchar();
    while (isdigit(c))
    {
        ans = (ans << 3) + (ans << 1) + c - '0';
        c = getchar();
    }
    return ans;
}
struct data  //以結構體方式儲存便於排序
{
    int a, b, w;
} C[100005];
int cmp(data &a, data &b)
{
    return a.w > b.w;
}
int fa[40005], rank[40005];  //以下為並查集
int find(int a)
{
    return (fa[a] == a) ? a : (fa[a] = find(fa[a]));
}
int query(int a, int b)
{
    return find(a) == find(b);
}
void merge(int a, int b)
{
    int x = find(a), y = find(b);
    if (rank[x] >= rank[y])
        fa[y] = x;
    else
        fa[x] = y;
    if (rank[x] == rank[y] && x != y)
        rank[x]++;
}
void init(int n)
{
    for (int i = 1; i <= n; ++i)
    {
        rank[i] = 1;
        fa[i] = i;
    }
}
int main()
{
    int n = read(), m = read();
    init(n * 2); //對於罪犯i,i+n為他的敵人
    for (int i = 0; i < m; ++i)
    {
        C[i].a = read();
        C[i].b = read();
        C[i].w = read();
    }
    std::sort(C, C + m, cmp);
    for (int i = 0; i < m; ++i)
    {
        if (query(C[i].a, C[i].b))  //試圖把兩個已經被標記為“朋友”的人標記為“敵人”
        {
            printf("%d\n", C[i].w); //此時的怒氣值就是最大怒氣值的最小值
            break;
        }
        merge(C[i].a, C[i].b + n);
        merge(C[i].b, C[i].a + n);
        if (i == m - 1)  //如果迴圈結束仍無衝突,輸出0
            puts("0");
    }
    return 0;
}

剛才我說,種類並查集可以維護敵人的敵人是朋友這樣的關係,這種說法不夠準確,較為本質地說,種類並查集(包括普通並查集)維護的是一種迴圈對稱的關係。

BbFNwj.png

所以如果是三個及以上的集合,只要每個集合都是等價的,且集合間的每個關係都是等價的,就能夠用種類並查集進行維護。例如下面這道題:

題目描述

動物王國中有三類動物 A,B,C,這三類動物的食物鏈構成了有趣的環形。A 吃 B,B 吃 C,C 吃 A。

現有 N 個動物,以 1 - N 編號。每個動物都是 A,B,C 中的一種,但是我們並不知道它到底是哪一種。

有人用兩種說法對這 N 個動物所構成的食物鏈關係進行描述:

  • 第一種說法是 1 X Y,表示 X 和 Y 是同類。
  • 第二種說法是2 X Y,表示 X 吃 Y 。

此人對 N 個動物,用上述兩種說法,一句接一句地說出 K 句話,這 K 句話有的是真的,有的是假的。當一句話滿足下列三條之一時,這句話就是假話,否則就是真話。

  • 當前的話與前面的某些真的話衝突,就是假話
  • 當前的話中 X 或 Y 比 N 大,就是假話
  • 當前的話表示 X 吃 X,就是假話

你的任務是根據給定的 N 和 K 句話,輸出假話的總數。

輸入格式

第一行兩個整數,N,K,表示有 N 個動物,K 句話。

第二行開始每行一句話(按照題目要求,見樣例)

輸出格式

一行,一個整數,表示假話的總數。

我們會發現 A、B、C三個種群天然地符合用種類並查集維護的要求。

BbFtmQ.png

於是我們可以用一個三倍大小的並查集進行維護,用i+n表示i的捕食物件,而i+2n表示i的天敵。

#include <cstdio>
#include <cctype>
int read()
{
    int ans = 0;
    char c = getchar();
    while (!isdigit(c))
        c = getchar();
    while (isdigit(c))
    {
        ans = (ans << 3) + (ans << 1) + c - '0';
        c = getchar();
    }
    return ans;
}
int fa[150005], rank[150005];
int find(int a)
{
    return (fa[a] == a) ? a : (fa[a] = find(fa[a]));
}
int query(int a, int b)
{
    return find(a) == find(b);
}
void merge(int a, int b)
{
    int x = find(a), y = find(b);
    if (rank[x] >= rank[y])
        fa[y] = x;
    else
        fa[x] = y;
    if (rank[x] == rank[y] && x != y)
        rank[x]++;
}
void init(int n)
{
    for (int i = 1; i <= n; ++i)
    {
        rank[i] = 1;
        fa[i] = i;
    }
}
int main()
{
    int n = read(), m = read(), ans = 0;
    init(n * 3); //i吃i+n,被i+2n吃
    for (int i = 0; i < m; ++i)
    {
        int opr, x, y;
        scanf("%d%d%d", &opr, &x, &y);
        if (x > n || y > n) //特判x或y不在食物鏈中的情況
            ans++;
        else if (opr == 1)
        {
            if (query(x, y + n) || query(x, y + 2 * n)) //如果已知x吃y,或者x被y吃,說明這是假話
                ans++;
            else
            {
                merge(x, y);                 //這是真話,則x和y是一族
                merge(x + n, y + n);         //x的獵物和y的獵物是一族
                merge(x + 2 * n, y + 2 * n); //x的天敵和y的天敵是一族
            }
        }
        else if (opr == 2)
        {
            if (query(x, y) || query(x, y + 2 * n)) //如果已知x與y是一族,或者x被y吃,說明這是假話
                ans++;
            else
            {
                merge(x, y + n);         //這是真話,則x吃y
                merge(x + n, y + 2 * n); //x的獵物吃y的獵物
                merge(x + 2 * n, y);     //x的天敵吃y的天敵,或者說y吃x的天敵
            }
        }
    }
    printf("%d\n", ans);
    return 0;
}

相關文章