圖論專題-學習筆記:並查集
1.概述
並查集是一種資料結構,用於圖論之中(更多時候用於樹),通常用來維護一張無向圖內 \(O(1)\) 判斷兩個點是否在同一個連通塊內,有很多的用法,擴充套件性也比較高。
2.模板
下面還是通過一道模板講解並查集的用法。
我們假設這 4 個元素分別表示 4 個人。假設每個人都會在一個群內,第 \(i\) 個人的群主表示為 \(fa_i\) (其實如果抽象成一棵樹,就是 \(i\) 的父親節點)
初始時,每一個人單獨在一個群內,則令 \(fa_i=i\)
看操作 1 :問第 1 個人與第 2 個人在不在同一個群內。
顯然不在了,大家各管各的,輸出 N
。
操作 2:合併第 1 個人與第 2 個人所在的群。
如何合併呢?此時兩個人分屬於不同的群,現在要將兩個人合成一個群,那麼我們直接把 2 的群主改成 1 不就可以了?即令 \(fa_2=1\) 。從樹的角度看,初始時每一個點都是單獨的根節點,現在在 1 和 2 之間連一條邊,生成一棵新樹,同時令 1 為根節點。
接下來又問 1 與 2 在不在一個群內。
這一步是並查集的判斷操作,判斷時我們發現,\(fa_1=fa_2\)
Y
。
此時群組情況如下所示:
1 3 4
\
2
接下來合併 3 4.仿照上述步驟,令 \(fa_4=3\) 。
這裡說明一下,其實令 \(fa_3=4\) 也是可以的,看個人習慣,本質上並沒有什麼區別,畢竟都在一個群裡面,誰是群主都沒有問題。
群組情況 :
1 3
\ \
2 4
下一個操作詢問 1 4 在不在一個群內,\(fa_1=1\),\(fa_3=3\) ,群主不一樣,不在一個群內,輸出 N
。
下一步合併 2 3,此時。。。。。。
1 不同意了!如果我們修改 \(fa_3=2\) ,沒有什麼問題(具體為什麼見下文),但是萬一程式讓 \(fa_2=3\)
既然 2 搞不定,我們直接找最高群主 1 談談,直接令 \(fa_1=3\) 就可以解決了。從樹的角度看,就是改變根節點的父親。
群主情況:
3
/ \
1 4
\
2
接下來問 1 4 在不在一個群內, \(fa_1=fa_4\) ,在一個群內,輸出 Y
。
然而此時,如果再來一個詢問:詢問 2 4在不在一個群內,要怎麼辦呢?
肉眼可見, 2 4 在一個群內,應該輸出 Y
,然而我們上面的判斷都是根據 \(fa_i\) 是否相等判斷的,此時並不相等,不就出問題了嗎?
為了解決這個問題,方法是:找到最高群主也就是根節點。
看圖,2 的群主是 1 ,而 1 的群主是 3 ,這樣 2 的最高群主不就是 3 了嗎?4 的最高群主也是 3 ,在同一個群內。
如果此時又來一個問題:判斷現在有幾個群要怎麼辦呢?
由於每一個群都有最高群主,且只有最高群主的群主是自己(為什麼?),那麼只要統計出有幾個 \(i \in [1,n]\) 使得 \(fa_i==i\) 即可。也就是找出每一棵樹的根節點。
完美解決~~~
程式碼實現:
- 初始化:
這裡直接一遍 for 即可。for(int i=1;i<=n;i++) fa[i]=i;
- 查詢某個節點的最高群主也就是根節點。
遞迴查詢即可,程式碼如下:
其中, \(fa_x==x\) 表示找到根節點了(根節點的父親就是根節點,初始化時已經這樣操作過了),找到返回 \(x\) ,否則遞迴查詢。int gf(int x) {return (fa[x]==x)?x:gf(fa[x]);}
然而你以為這樣就結束了嗎?看下圖:
這樣,查詢一次 \(fa_{op}\) 就要 \(O(1e5或1e6)\) 的時間複雜度,多查幾次不就 TLE 了?為了解決這個問題,我們引入一個優化:路徑壓縮。1-2-3-4-5-6-7-......-op(某極大的數字,比如說 1e5 1e6 之類的)
路徑壓縮的目的就是為了解決上面的問題,即在查詢某節點的祖先的時候,我們將一路上查詢的所有節點的父親全部連到根節點,也就是變成下圖:
這樣,查詢複雜度直接降至 \(O(1)\) ,大大優化查詢複雜度。1 | \ \ \ \ 2 3 4 ... op
而程式碼只需要這樣改:int gf(int x) {return (fa[x]==x)?x:fa[x]=gf(fa[x])}
- 合併操作:
找到根節點合併即可。void hb(int x,int y) {if(gf(x)!=gf(y)) fa[fa[x]]=fa[y];} //由於加入了路徑壓縮所以不會有問題。
- 查詢操作
判斷兩個點的祖先是否相同即可。cout<<((gf(x)==gf(y))?'Y':'N')<<"\n"; //實測這裡三目運算子外面不加括號會CE
- 統計樹的數量
根據上述所講,一遍 for 即可。for(int i=1;i<=n;i++) if(fa[i]==i) ans++;
程式碼:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+10;
int n,m,fa[MAXN];
int gf(int x) {return (fa[x]==x)?x:fa[x]=gf(fa[x]);}
void hb(int x,int y) {if(gf(x)!=gf(y)) fa[fa[x]]=fa[y];}
int read()
{
int sum=0,fh=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(fh=='-') fh=-1;ch=getchar();}
while(ch>='0'&&ch<='9') {sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
return sum*fh;
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
int x,y,z;
z=read();x=read();y=read();
if(z==1) hb(x,y);
else cout<<((gf(x)==gf(y))?'Y':'N')<<"\n";
}
return 0;
}
如果你看懂了上述程式碼,那麼恭喜你,學會了並查集的基礎操作!
接下來,你將會見到各路例題以及並查集的各種神奇用法。
3.例題
題單:
- 入門題:
- [BOI2003]團伙
- 與別的演算法結合:
- 搭配購買
- 關押罪犯
- 考思維的題:
- [JSOI2008]星球大戰
- [IOI2014]game 遊戲
- 二維轉一維:
- [USACO14JAN]Ski Course Rating G
- 小 D 的地下溫泉
- 擴充套件域並查集&邊帶權並查集:
- [NOI2001]食物鏈
- [NOI2002]銀河英雄傳說
- [CEOI1999]Parity Game
1.入門題:
這道題是一道練手題,思維與演算法難度都不高,就是一個並查集。
首先處理讀入資料,將是朋友的人合併,是敵人的人先存在 \(v\) 數組裡面(使用 vector ,不會的請自行查百度)。
然後根據我的敵人的敵人是我的朋友,三重迴圈再合併一次即可。
程式碼(篇幅有限,只放部分程式碼,下同):
const int MAXN=1000+10;
int n,m,ans,fa[MAXN];
vector<int>v[MAXN];
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
char op;int p,q;
cin>>op;p=read();q=read();
if(op=='F') hb(p,q);
else
{
v[p].push_back(q);
v[q].push_back(p);
}
}
for(int i=1;i<=n;i++)
for(int j=0;j<v[i].size();j++)
for(int k=0;k<v[v[i][j]].size();k++) hb(i,v[v[i][j]][k]);
for(int i=1;i<=n;i++) if(gf(i)==i) ans++;
cout<<ans<<"\n";
return 0;
}
2.與別的演算法結合:
賣雲朵可還行
這道題首先,要同時買兩朵雲的操作就很像並查集,因此我們可以考慮使用並查集來求解(通常題目當中出現了 “同時....” / “一起....” 等字眼都有可能是並查集)。
然後,又看到要買雲朵,每種雲朵只有一份,錢數又是有限的,濃濃的透露出 0/1 揹包 的氣息。
因此,本道題的演算法為:並查集 + 0/1 揹包
首先將必須同時購買的物品合併,然後將雲朵組成的一棵棵樹中所有節點的 \(c_i,d_i\) 全部加起來,放到新陣列 \(money_j,value_j\) 中,跑一遍 0/1 揹包即可求解。
程式碼:
const int MAXN=1e4+10;
int n,m,w,c[MAXN],d[MAXN],money[MAXN],value[MAXN],fa[MAXN],ys[MAXN],tmp,f[MAXN];
int main()
{
n=read();m=read();w=read();
for(int i=1;i<=n;i++) {c[i]=read();d[i]=read();fa[i]=i;}
for(int i=1;i<=m;i++)
{
int x,y;
x=read();y=read();
hb(x,y);
}//合併操作
for(int i=1;i<=n;i++) if(gf(i)==i) ys[i]=++tmp;//處理出最後物品個數
for(int i=1;i<=n;i++)
{
money[ys[fa[i]]]+=c[i];
value[ys[fa[i]]]+=d[i];
}//算出 money[i] 和 value[i]
for(int i=1;i<=tmp;i++)
for(int j=w;j>=money[i];j--)
f[j]=Max(f[j],f[j-money[i]]+value[i]);// 0/1 揹包
cout<<f[w]<<"\n";
return 0;
}
這道題可以使用二分圖來解,那麼如何使用並查集來解呢?
由於要想辦法讓最大值最小,所以使用二分?
No,這道題不需要使用二分,而是貪心即可。想一想,我們只需要儘量將怒氣值大的罪犯組拆掉不就好了,碰到第一個不能拆掉的就是答案。
因此,這道題的演算法為:並查集 + 貪心。
首先,按照怒氣值從大到小排序一遍。
然後,我們令 \(d_i\) 表示 \(i\) 的第一個會與他發生摩擦的人,初始化為 0 。
接下來處理資料。假設此時我們要處理 \(a\) 與 \(b\) 發生摩擦,怒氣值為 \(c\) 的資訊:
- 如果此時 \(a,b\) 已經在一起了,直接輸出 \(c\) ,結束程式。
- 否則,他們不在一起,以 \(a\) 為例:如果 \(d_a=0\),說明此時沒有人與他有摩擦,則 \(d_a=b\) ,否則說明已經有人與他有摩擦了,由於只有兩個監獄,那麼合併 \(b,d_a\) 即可。
- 正確性:顯然要將 \(d_a,a\) 拆掉。假設 \(b,d_a\) 之間的怒氣值(如果沒有摩擦為 0)為 \(c'\) ,根據之前的排序,必然有 \(c'<c\),那麼顯然合併 \(b,d_a\) 比合並 \(a,b\) 更優。
程式碼:
const int MAXN=20000+10,MAXM=100000+10;
int n,m,fa[MAXN],d[MAXN];
struct node
{
int a,b,c;
}a[MAXM];
int main()
{
n=read();m=read();
for(int i=1;i<=m;i++) {a[i].a=read();a[i].b=read();a[i].c=read();}
for(int i=1;i<=n;i++) fa[i]=i;
sort(a+1,a+m+1,cmp);//自行打 cmp 函式
for(int i=1;i<=m;i++)
{
if(gf(a[i].a)!=gf(a[i].b))
{
if(!d[a[i].a]) d[a[i].a]=a[i].b;
else hb(d[a[i].a],a[i].b);
if(!d[a[i].b]) d[a[i].b]=a[i].a;
else hb(d[a[i].b],a[i].a);
}
else {cout<<a[i].c<<"\n";return 0;}
}
cout<<"0\n";
return 0;
}
3.考思維的題:
正常的並查集支援合併操作,但是不支援刪除操作,然而這道題的所有操作不是合併就是刪除,那麼要怎麼辦呢?
既然並查集支援合併操作,那麼我們想辦法支援合併操作就好了唄!
我們將打擊星球的順序 倒過來操作 ,將其視作 重建星球 ,然後每重建一個合併一次,最後處理連通塊個數不就好了qwq。
關於如何處理連通塊個數,這裡提供一個 \(O(1)\) 的思想:
- 初始化 \(sum=n\) ,表示有 \(n\) 個連通塊。
- 每合併兩個點,\(sum--\) 。
- 注意合併的兩個點不能在同一個連通塊內。
注意:最後輸出答案時要逆序輸出,沒有重建的星球不算在答案內,只有當兩個星球全部重建完成才能合併。這裡我使用 \(book\) 陣列標記是否合併完成。
程式碼:
const int MAXN=4e5+10;
int n,fa[MAXN],m,k,des[MAXN],ans[MAXN],sum;
bool book[MAXN];
vector<int>v[MAXN];
void hb(int x,int y) {if(gf(x)!=gf(y)) {sum--;fa[fa[x]]=fa[y];}}
//注意 sum--;,gf(x)略
int main()
{
n=read();m=read();
for(int i=0;i<n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
int x=read();int y=read();
v[x].push_back(y);v[y].push_back(x);
}
k=read();sum=n;
for(int i=1;i<=k;i++) book[des[i]=read()]=1;
for(int i=0;i<n;i++)
if(!book[i])
for(int j=0;j<v[i].size();j++)
if(!book[v[i][j]]) hb(i,v[i][j]);
for(int zzh=k;zzh>=1;zzh--)
{
ans[zzh]=sum-zzh;
book[des[zzh]]=0;
for(int i=0;i<v[des[zzh]].size();i++)
if(!book[v[des[zzh]][i]]) hb(v[des[zzh]][i],des[zzh]);
}
ans[0]=sum;
for(int i=0;i<=k;i++) cout<<ans[i]<<"\n";
return 0;
}
別看這道題是 IOI 的題目,其實想通了真的非常簡單。你看評級都是綠色
首先,為了讓梅玉只有到最後一個詢問才能判斷是否連通,這裡就有一種思路:我們構造某一張圖使得這張圖連通,且最後一個詢問問的點 \(x,y\) 會連一條邊,這裡記作 \(x->y\) ,但是一旦我們刪去了 \(x->y\) 整張圖就會裂成兩個集合,換句話說, \(x->y\) 是這一張圖的橋/割邊。(橋/割邊的定義:如果刪除某條 \(u->v\) 的邊後途中連通塊個數增加,那麼 \(x->y\) 是這張圖的橋/割邊)
為什麼正確呢?如果 \(x->y\) 是這張圖的割邊,那麼在倒數第二個詢問中梅玉依然不能判斷整張圖是否連通(有 2 個連通塊),此時她必須再問一次才能確定圖是否連通。
因此思路就很明確了,我們對於某個詢問 \(a->b\) ,如果合併 \(a,b\) 以後 \(x,y\) 在一個連通塊內,這顯然不是我們想要的操作,此時不能合併 \(a,b\) ;否則,合併 \(a,b\) 。最後不要忘記輸出最後一條邊是否連通,這裡輸出 0
或 1
都可以。不過根據我們構造的方案,最好輸出 1
。(實測 0
也能夠通過)
當然,本部落格只是提供了其中一種思路,具體別的思路也請各位發現然後解決。
所以你看,IOI的題也不見得非常難
程式碼:
const int MAXN=1500+10;
int n,m,fa[MAXN],q1[MAXN*MAXN],q2[MAXN*MAXN];
int main()
{
n=read();m=n*(n-1)/2;
for(int i=1;i<=m;i++) {q1[i]=read();q2[i]=read();}
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<m;i++)
{
int x=q1[i],y=q2[i];
int fx=gf(x),fy=gf(y);
int x1=q1[m],y1=q2[m];
int fx1=gf(x1),fy1=gf(y1);
if(fx>fy) swap(fx,fy);
if(fx1>fy1) swap(fx1,fy1);
if(fx==fx1&&fy==fy1) cout<<"0\n";//判斷 最後兩個點 合併之後是否在同一集合內,在就不合並
else
{
cout<<"1\n";
hb(x,y);
}//否則合併
}
cout<<"1\n";//輸出 1 也可以
return 0;
}
4.二維轉一維:
[USACO14JAN]Ski Course Rating G
這道題也是一道與貪心相結合的題目。
首先,我們需要將二維的地圖轉成一維:對於 \((i,j)\) (第 \(i\) 行第 \(j\) 列,下同),我們將其在一維編號為 \((i-1)*m+j\) (注意不是 \(n\)),然後對於相鄰的兩個二維的點連一條邊,將點壓成一維後按照邊權從小到大排序。
然後,對於每一條邊:
- 首先,如果這條邊連著的兩個點在一個連通塊內,
continue;
。 - 然後,如果兩棵子樹 \(size\) 和大於等於 \(t\),那麼 \(ans+=c*(cnt_i)\)(\(cnt_i\) 見下文)。 其中, \(i\) 為兩個子樹的編號,且 \(size_i<t\) 。為什麼大於 \(t\) 的就不能統計了呢?因為之前 \(size_i<t\) 的時候已經統計過一次,此時又統計就會造成浪費,並且即使有新的起點加入,也已經被統計過,沒有意義了。
- 而後合併兩個點,如果這兩個點內有起點,我們就將增加的起點個數存在 \(cnt_i\) 裡面。
程式碼:
const int MAXN=500+10;
int t,n,m,a[MAXN][MAXN],b[MAXN][MAXN],fa[MAXN*MAXN],size[MAXN*MAXN],tmp,cnt[MAXN*MAXN];
typedef long long LL;
LL ans;//不開long long見祖宗
struct node
{
int a,b,c;
}dis[2*MAXN*MAXN];
int main()
{
//讀入略,a=地圖,b=是否為起點
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(j!=m)
{
tmp++;
dis[tmp].a=turn(i,j);dis[tmp].b=turn(i,j+1);dis[tmp].c=abs(a[i][j]-a[i][j+1]);
}
if(i!=n)
{
tmp++;
dis[tmp].a=turn(i,j);dis[tmp].b=turn(i+1,j);dis[tmp].c=abs(a[i][j]-a[i+1][j]);
}
if(b[i][j]==1) cnt[turn(i,j)]=1;
}//連邊操作
for(int i=1;i<=n*m;i++) fa[i]=i,size[i]=1;
sort(dis+1,dis+tmp+1,cmp);
for(int i=1;i<=tmp;i++)
{
int fx=gf(dis[i].a),fy=gf(dis[i].b);
if(fx==fy) continue;
if(size[fy]+size[fx]>=t)
{
if(size[fy]<t) ans+=(LL)dis[i].c*cnt[fy];
if(size[fx]<t) ans+=(LL)dis[i].c*cnt[fx];
}
if(size[fx]>size[fy]) swap(fx,fy);
fa[fx]=fy;
size[fy]+=size[fx];cnt[fy]+=cnt[fx];//注意更新答案
}
cout<<ans<<"\n";
return 0;
}
這道題類似,首先二維轉一維不說,然後如果兩個相鄰點都是泉水合並。
詢問操作:直接求出詢問點所在樹的 \(size\) 即可,求個最大值。
有個坑點:當心所有點都是土地,此時我們需要輸出 1
。
修改操作:泉水改土地直接修改地圖然後 \(size--\) 即可。土地改泉水時我們需要新開一個點,改變地圖後令新開的點 \(fa=自己,size=1\) ,然後四周合併一遍即可。注意不能直接在原點上修改,否則會有很多奇奇怪怪的問題。
程式碼:
const int MAXN=1e6+10;
int n,m,fa[MAXN<<1],size[MAXN<<1],q,ys[MAXN<<1],tmp;
int Next[4][2]={{0,1},{1,0},{0,-1},{-1,0}};
char a[MAXN];
//gf(),turn()略
void hb(int x,int y) {if(gf(x)!=gf(y)) {if(size[fa[y]]>size[fa[x]]) swap(x,y);size[fa[y]]+=size[fa[x]];fa[fa[x]]=fa[y];}}
int main()
{
n=read();m=read();tmp=n*m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[turn(i,j)];
for(int i=1;i<=n*m;i++) {fa[i]=i;size[i]=((a[i]=='.')?1:0);}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
ys[turn(i,j)]=turn(i,j);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(i!=1)
{
if(a[turn(i,j)]=='.'&&a[turn(i-1,j)]=='.') hb(turn(i,j),turn(i-1,j));
}
if(j!=1)
{
if(a[turn(i,j)]=='.'&&a[turn(i,j-1)]=='.') hb(turn(i,j),turn(i,j-1));
}
}
q=read();
for(int i=1;i<=q;i++)
{
int op,w;
op=read();w=read();
if(op==1)
{
int flag=1,ans=0;
for(int j=1;j<=w;j++)
{
int x,y;
x=read();y=read();
if(a[turn(x,y)]=='.'&&size[gf(ys[turn(x,y)])]>ans)
{
ans=size[gf(ys[turn(x,y)])];
flag=j;
}
}
cout<<flag<<"\n";
}
else
{
for(int j=1;j<=w;j++)
{
int x,y;
x=read();y=read();
if(a[turn(x,y)]=='.')
{
a[turn(x,y)]='*';
size[gf(ys[turn(x,y)])]--;
}
else
{
ys[turn(x,y)]=++tmp;
a[turn(x,y)]='.';fa[ys[turn(x,y)]]=ys[turn(x,y)];size[ys[turn(x,y)]]=1;
for(int k=0;k<4;k++)
{
int tx=x+Next[k][0];
int ty=y+Next[k][1];
if(tx>0&&ty>0&&tx<=n&&ty<=m&&a[turn(tx,ty)]=='.') hb(ys[turn(x,y)],ys[turn(tx,ty)]);
}
}
}
}
}
return 0;
}
5.擴充套件域並查集&邊帶權並查集:
這道題我是用擴充套件域求解的,各位讀者可以嘗試使用邊帶權求解 (其實是我不會)
擴充套件域的原理:擴大並查集的上限來滿足題目需要。
這道題,我們擴大並查集上線至 \(3*n\) ,由於不知道哪個動物在哪個組,令 \(1...n\) , \(n+1...2*n\) , \(2*n+1...3*n\)為三個組,\(x,x+n,x+2*n\) 表示同一個動物,如果是組內元素同祖先,表示他們是同類關係;如果是跨組同祖先,表示捕食關係,本題規定如果 \(gf(x)==gf(y+n)||gf(x+n)==gf(y+n+n)||gf(x+n+n)==gf(y)\) 那麼 \(x\) 吃 \(y\) 。
如何判定一句話與前面的真話是矛盾的呢?
如果一句話告訴你 \(x,y\) 是同類,但是事實是 \(x\) 吃 \(y\) 或者 \(y\) 吃 \(x\) ,那麼是假的,否則是真的,合併 \((x,y)\),\((x+n,y+n)\),\((x+n+n,y+n+n)\)。注意都要合併,否則傳遞不及時可能會導致一些錯誤。
如果一句話告訴你 \(x\) 吃 \(y\) ,但是事實是 \(x,y\) 是同類或者 \(y\) 吃 \(x\) ,那麼是假的,否則是真的,合併 \((x,y+n)\),\((x+n,y+n+n)\),\((x+n+n,y)\)。
然後就做完了。如果實在看不懂我的題解,還可以看一看 luogu 題目裡面的題解,或許能夠更好的理解。
程式碼:
const int MAXN=5e4+10;
int n,k,fa[MAXN*3],ans=0;
int main()
{
n=read();k=read();
for(int i=1;i<=n*3;i++) fa[i]=i;
for(int i=1;i<=k;i++)
{
int op,x,y;
op=read();x=read();y=read();
if(x>n||y>n) ans++;
else if(op==2&&x==y) ans++;
else
{
if(op==1)
{
if(gf(x)==gf(y+n)||gf(x+n)==gf(y)) ans++;
else hb(x,y),hb(x+n,y+n),hb(x+n+n,y+n+n);
}
else
{
if(gf(x)==gf(y)||gf(y)==gf(x+n)) ans++;
else hb(x,y+n),hb(x+n,y+n+n),hb(x+n+n,y);
}
}
}
cout<<ans<<"\n";
return 0;
}
這道題使用邊帶權並查集來做。注意這一題的合併具有一定的方向性。
首先,我們令 \(front_i\) 表示 \(i\) 到根節點(領頭羊)的距離,初始化為 0。\(num_i\) 表示以 \(i\) 為根節點的樹的大小,初始化為 1。
然後,由於戰隊是一條鏈,但是我們路徑壓縮之後變成了一棵樹,因此在路徑壓縮時先要加入這樣一句話:
front[x]+=front[fa[x]]
保證 \(front\) 更新及時,然後才能路徑壓縮。這裡又要注意,要先計算出 \(gf(fa[x])\) 並且存下之後才能更新,否則資料不夠及時。
合併操作的時候,假設我們將 \(x\) 接到 \(y\) 後面,此時令 \(fx=gf(x),fy=gf(y)\) ,要讓 \(fa_{fx}=num_{fy}\) ,因為此時此刻 \(x\) 不是祖先了,需要更新 \(front_{fx}\) ,不過不用著急將更新下傳到孩子節點,因為路徑壓縮會幫你做好的qwq。
此時,由於 \(fy\) 後面加入了 \(num_{fx}\) 個節點,需要更新 \(num_{fy}+=num{fx}\) ,然後清零 \(num_{fx}\) 。
統計答案時,不在一個集合內輸出 -1
,否則輸出 \(|front_{x}-front_{y}|-1\) ,具體為什麼請各位讀者思考。
程式碼:
const int MAXN=30000+10;
int t,fa[MAXN],front[MAXN],num[MAXN];
int gf(int x)
{
if(fa[x]==x) return x;
int f=gf(fa[x]);
front[x]+=front[fa[x]];
return fa[x]=f;
}
int main()
{
t=read();
for(int i=1;i<=30000;i++) fa[i]=i,front[i]=0,num[i]=1;
for(int i=1;i<=t;i++)
{
char ch;int x,y;
cin>>ch;x=read();y=read();
if(ch=='M')
{
int fx=gf(x);
int fy=gf(y);
if(fx!=fy)
{
front[fx]=num[fy];
num[fy]+=num[fx];
num[fx]=0;
fa[fx]=fy;
}
}
else
{
if(gf(x)!=gf(y)) cout<<"-1\n";
else cout<<abs(front[x]-front[y])-1<<"\n";
}
}
return 0;
}
這道題兩種做法都可以,不過個人認為擴充套件域並查集更好想也更好寫。
將並查集容量擴大 2 倍,如果奇偶性相同則合併 \((x,y),(x+n,y+n)\),否則合併 \((x,y+n),(x+n,y)\) 。如果兩個點已經在同一個集合內,仿照上例直接判斷即可。
考慮到 \(n\) 很大,\(m\) 很小,需要先離散化每一個點。(不會離散化自行百度)
程式碼:
const int MAXN=1e5+10;
int n,m,a[MAXN],fa[MAXN],tmp,l[MAXN],r[MAXN],q[MAXN];
int main()
{
n=read();m=read();
for(int i=1;i<=m;i++)
{
string str;
l[i]=read();r[i]=read();cin>>str;
q[i]=(str=="odd")?1:0;
a[++tmp]=l[i]-1;a[++tmp]=r[i];//注意存的是l[i]-1,這裡有字首和的思想
}
sort(a+1,a+tmp+1);
n=unique(a+1,a+tmp+1)-a-1;//離散化
for(int i=0;i<=(n<<1);i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
int x=lower_bound(a+1,a+n+1,l[i]-1)-a;
int y=lower_bound(a+1,a+n+1,r[i])-a;//找到離散化的點
//非C++選手請自行打二分,C++選手不懂得查百度
if(q[i]==1)
if(gf(x)==gf(y)||gf(x+n)==gf(y+n)) {cout<<i-1<<"\n";return 0;}
else hb(x,y+n),hb(x+n,y);
else
if(gf(x+n)==gf(y)||gf(x)==gf(y+n)) {cout<<i-1<<"\n";return 0;}
else hb(x,y),hb(x+n,y+n);
}
cout<<m<<"\n";
return 0;
}
4.總結
相信做完上述這 億 一些例題後,各位都對並查集有了一定的瞭解。不過這些只是並查集的初等應用,並查集還有很多高階版本,比如可持久化並查集。這裡不講這些,太高深 且作者本人不會。並查集很多時候用於圖論之中,或者是判斷是否在同一個集合內。