【學習筆記】並查集應用

FlyPancake發表於2024-07-30

【學習筆記】並查集應用

NOI 2001 食物鏈 為例の兩種並查集用法。

題目大意:

規定每隻動物有且僅有三種可能的種類 \(A、B、C\)\(A\) 會吃 \(B\)\(B\) 會吃 \(C\)\(C\) 會吃 \(A\)

給定 \(N\) 只動物,\(K\) 個語句。每個語句有如下兩種可能的表達:

  1. 1 X Y 表示動物 \(X\) 與動物 \(Y\) 是同類。

  2. 2 X Y 表示動物 \(X\)\(Y\)

每個語句可能是真話也可能是假話,每個語句是假話有三種可能:

  1. \(X\)\(Y\)\(N\) 大。

  2. 表達為 \(X\)\(X\)

  3. 當前的話與前面的某些真的話衝突。

請求出 \(K\) 個語句裡假話的總數。

種類並查集(擴充套件域並查集)

先推一個講解

並查集能維護連通性、傳遞性,通俗地說,親戚的親戚是親戚。

然而當我們需要維護一些對立關係,比如敵人的敵人是朋友時,正常的並查集就很難滿足我們的需求。

這時,種類並查集就誕生了。

同個種類的並查集中合併,表達他們是朋友這個含義。

不同種類的並查集中合併,表達他們是敵人這個含義。


此題關係有三類(\(A、B、C\)),所以我們考慮建立 3 倍大小的並查集。其中 \(1 \sim n\) 表示種類 \(A\)\(n+1 \sim 2n\) 表示種類 \(B\)\(2n+1 \sim 3n\) 表示種類 \(C\)

如果兩隻動物 \(x\)\(y\) 是同類,那麼就將 \(A_x\)\(A_y\)\(B_x\)\(B_y\)\(C_x\)\(C_y\) 各併入一個集合內。

如果兩隻動物 \(x\)\(y\),那麼就將 \(A_x\)\(B_y\)\(B_x\)\(C_y\)\(C_x\)\(A_y\) 各併入一個集合內。

此時如果要表示動物 \(x\) 吃動物 \(y\),就說明 \(A_x\)\(B_y\) 在同一集合中,根據對稱性,其它的也一樣,所以判斷時只需要判一組。

  • \(x\)\(y\) 同類與 \(x\)\(y\)\(y\)\(x\) 矛盾。
  • \(x\)\(y\)\(x\)\(y\) 同類或 \(y\)\(x\) 矛盾。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5e4+5;

int fa[N*3];

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

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, k, ans = 0; cin>>n>>k;
    for(int i=1; i<=n*3; i++)
        fa[i] = i;
    while(k--){
        int op, x, y; cin>>op>>x>>y;
        if(x==y&&op==2 || x>n || y>n){
            ans++;
            continue;
        }
        if(op==1){
            if(find(x)==find(y+n) || find(y)==find(x+n)){
                ans++;
                continue;
            }
            fa[find(x)] = fa[find(y)];
            fa[find(x+n)] = fa[find(y+n)];
            fa[find(x+n+n)] = fa[find(y+n+n)];
        } else if(op==2){
            if(find(x)==find(y) || find(y)==find(x+n)){
                ans++;
                continue;
            }
            fa[find(x)] = fa[find(y+n)];
            fa[find(x+n)] = fa[find(y+n+n)];
            fa[find(x+n+n)] = fa[find(y)];
        }
    }
    cout<<ans;
    return 0;
}

帶權並查集

每個點與其集合的根都有權重,以此來表達關係。

以此題為例,0 代表 \(x\)\(fa_x\) 同類,1 代表 \(x\)\(fa_x\),2 代表 \(x\)\(fa_x\) 吃。

重點在於如何更新權值和判斷關係。權值更新肯定伴隨並查集的更新。在下面的圖中就如向量一般計算。

查詢(路徑壓縮)

知道 \(x\) 與其根 \(fa[x]\) 的關係,\(fa[x]\) 與其根 \(fa[fa[x]]\) 的關係,可以推出 \(x\)\(fa[fa[x]]\) 的關係。

注意這裡要先更新 \(fa[x]\) 的權值(先 find(fa[x])),在更新 \(x\) 的權值(得先存下 \(fa[x]\),不然 \(fa[x]\) 會變)。

\[rel[x \rightarrow rt] = rel[x \rightarrow fa]+rel[fa \rightarrow rt] \]

find.png

合併

知道 \(x\)\(fa[x]\) 的關係,\(y\)\(fa[y]\) 的關係,以及 \(x\)\(y\) 之間的關係,就可以知道 \(fa[x]\)\(fa[y]\) 的關係。

注意是 \(fa[x]\) 併到 \(fa[y]\) 上還是 \(fa[y]\) 併到 \(fa[x]\) 上。以下是 \(fa[x]\) 併到 \(fa[y]\) 上。

\[rel[fa[x]] = rel[y]-rel[x]+rel[x \rightarrow y] \]

merge.png

判斷關係(是否矛盾)

知道 \(x、y\) 與根的關係,就能推出 \(x\)\(y\) 的關係。(此時 \(x\)\(y\) 已經在同一個集合內)

\[rel[x \rightarrow y] = rel[x]-rel[y] \]

check.png

以上操作取模時注意減法,顯然此題模數為 \(3\)

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 50005;

int fa[N], rel[N];
// relation 存與根的關係
// 0--同類,1--能吃,2--被吃
const int p = 3;
int n, k, ans;

void init(){
    for(int i=1; i<=n; i++){
        fa[i] = i;
        rel[i] = 0;
        // 初始化跟自己的關係是同類
    }
}

int find(int x){
    if(fa[x] == x) return x;
    // 知道 x 與 fa[x] 的關係,fa[x] 與根的關係,可以推出 x 與根的關係
    // rel[x->rt] = rel[x->fa]+rel[fa->rt]
    int f = fa[x];
    fa[x] = find(fa[x]);
    rel[x] = (rel[x]+rel[f])%p;
    // 必須得分開寫,因為原來的 fa[x] 與根的關係會在 find(fa[x]) 的時候更新
    return fa[x];
}

void merge(int u, int v, int r){
    // U與rtU的關係,V與rtV的關係,以及UV之間的關係,就可以知道rtU和rtV的關係。
    // rtU 併到 rtV 上
    // rel[ru] = rel[v]-rel[u]+rel[u->v]
    int ru = find(u), rv = find(v);
    if(ru != rv){
        fa[ru] = rv;
        rel[ru] = (rel[v]-rel[u]+r+p)%p;
    }
}

bool check(int x, int y, int r){
    if(x>n || y>n) return false; // 不能比 n 大
    if(x==y && r==1) return false; // 不能吃自己
    if(find(x)==find(y)){
        // 知道x、y與根的關係,就能推出 x 與 y 的關係
        // rel[x->y] = rel[x]-rel[y]
        return r == (rel[x]-rel[y]+p)%p;
    }
    return true;
    // 還沒明確的關係就是可行的
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    cin>>n>>k;
    init();
    while(k--){
        int op, x, y; cin>>op>>x>>y;
        if(check(x, y, op-1)){
            merge(x, y, op-1);
        } else{
            ans++;
        }
    }
    cout<<ans;
    return 0;
}

相關文章