【學習筆記】並查集應用
以 NOI 2001 食物鏈 為例の兩種並查集用法。
題目大意:
規定每隻動物有且僅有三種可能的種類 \(A、B、C\),\(A\) 會吃 \(B\),\(B\) 會吃 \(C\),\(C\) 會吃 \(A\)。
給定 \(N\) 只動物,\(K\) 個語句。每個語句有如下兩種可能的表達:
-
1 X Y
表示動物 \(X\) 與動物 \(Y\) 是同類。 -
2 X Y
表示動物 \(X\) 吃 \(Y\)。
每個語句可能是真話也可能是假話,每個語句是假話有三種可能:
-
\(X\) 或 \(Y\) 比 \(N\) 大。
-
表達為 \(X\) 吃 \(X\)。
-
當前的話與前面的某些真的話衝突。
請求出 \(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]\) 會變)。
合併:
知道 \(x\) 與 \(fa[x]\) 的關係,\(y\) 與 \(fa[y]\) 的關係,以及 \(x\) 與 \(y\) 之間的關係,就可以知道 \(fa[x]\) 和 \(fa[y]\) 的關係。
注意是 \(fa[x]\) 併到 \(fa[y]\) 上還是 \(fa[y]\) 併到 \(fa[x]\) 上。以下是 \(fa[x]\) 併到 \(fa[y]\) 上。
判斷關係(是否矛盾):
知道 \(x、y\) 與根的關係,就能推出 \(x\) 與 \(y\) 的關係。(此時 \(x\) 與 \(y\) 已經在同一個集合內)
以上操作取模時注意減法,顯然此題模數為 \(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;
}