並查集(詳)
作用:判斷兩個點是否屬於同一個集合
引入:(例題【洛谷P1551 親戚】)
題意大致如下:輸入n表示有n個人,m對x,y表示x,y在同一集合裡,k對詢問表示查詢x,y是否在同一集合
這個問題的核心在於如何判斷x,y在同一集合
其中一種暴力做法是,
使用陣列fa[ ]表示:點x隸屬於fa[x]的集合
初始化:
那麼毫無疑問,初始化即為
for(int i=1;i<=n;++i) fa[i]=i;
因為最開始自己是屬於只包括自己的集合
updata:
for(int i=1;i<=m;++i){ int u,v; scanf("%d%d",&u,&v);while(u!=fa[u]) u=fa[u]; while(v!=fa[v]) v=fa[v]; fa[u]=v; }
對於updata,每次把每兩個點中的一個點的fa[ ]值修改,可以使這個集合擁有兩個性質:
1.該個集合中總有一個點x可以使fa[x]=x,暫且x稱為"集合頭",每個集合的集合頭都不同
2.不論從那個點用fa[ ]值進行遞迴,總可以找到x
我們就可以順利地用集合頭代表這個集合
其中fa[u]=v等效於fa[v]=u,都是把兩個集合合併成一個集合,所以實質上集合的合併等同於集合頭的合併。
query:
只要判定兩點的集合頭是否相同,若相同則是同一個集合
for(int i=1;i<=k;++i){ int u,v; scanf("%d%d",&u,&v); while(u!=fa[u]) u=fa[u]; while(v!=fa[v]) v=fa[v]; if(u==v) printf("Yes\n"); else printf("No\n"); }
貼:
所以放一下暴力程式碼,暴力O(mn)仍然可以水過這題.....
#include<bits/stdc++.h> #define maxn 1000010 using namespace std; int n,m,k,fa[maxn];int main(){ scanf("%d%d%d",&n,&m,&k); for(int i=1;i<=n;++i) fa[i]=i; for(int i=1;i<=m;++i){ int u,v; scanf("%d%d",&u,&v); while(u!=fa[u]) u=fa[u]; while(v!=fa[v]) v=fa[v]; fa[u]=v; } for(int i=1;i<=k;++i){ int u,v; scanf("%d%d",&u,&v); while(u!=fa[u]) u=fa[u]; while(v!=fa[v]) v=fa[v]; if(u==v) printf("Yes\n"); else printf("No\n"); } return 0; }
優化:
我們可以發現大量的時間用在了遞迴尋找集合點的過程上
如果在每次尋找的過程中把集合中的點直接連在集合頭上,那麼下次尋找就可以大大縮短尋找時間
路徑合併:
一種基本的加速,有了路徑合併的才叫並查集
本題中加速後的的複雜度大致是O(klogn),這個不給出證明了,比較複雜
若此時將把7的集合合併進5的集合裡
#include<bits/stdc++.h> #define maxn 1000010 using namespace std; int n,m,k,fa[maxn]; int find(int x){ int k=x; while(k!=fa[k]) k=fa[k]; while(x!=fa[x]){ int tmp=x; x=fa[x]; fa[tmp]=k; } return k; } int main(){ scanf("%d%d%d",&n,&m,&k); for(int i=1;i<=n;++i) fa[i]=i; for(int i=1;i<=m;++i){ int u,v; scanf("%d%d",&u,&v); fa[find(u)]=find(v); } for(int i=1;i<=k;++i){ int u,v; scanf("%d%d",&u,&v); u=find(u); v=find(v); if(u==v) printf("Yes\n"); else printf("No\n"); } return 0; }
可以發現有明顯的加速
常見版本:
更為常見的版本通常是遞迴進行的,因為這樣方便(碼量少),而且經常用合併函式進行
集合合併也包括集合附帶屬性的合併,用函式會顯得比較簡潔
#include<bits/stdc++.h> #define maxn 100010 using namespace std; int n,m,k,fa[maxn]; int find(int x){ if(x!=fa[x]) fa[x]=find(fa[x]); return fa[x]; } void merge(int x,int y){ int fa1=find(x),fa2=find(y); if(fa1==fa2) return; else fa[fa1]=fa2; } int main(){ scanf("%d%d%d",&n,&m,&k); for(int i=1;i<=n;++i) fa[i]=i; for(int i=1;i<=m;++i){ int u,v; scanf("%d%d",&u,&v); merge(u,v); } for(int i=1;i<=k;++i){ int u,v; scanf("%d%d",&u,&v); int fa1=find(u),fa2=find(v); if(fa1==fa2) printf("Yes\n"); else printf("No\n"); } return 0; }
關於並查集按順序連邊:
並查集只能合併集合不能取消,如果要求減邊,那麼先用並查集合併到最後的結果再反過來加邊即可。
如果按一定順序連邊就按照那個規則連就行了。
例題【洛谷P1111 修復公路】(按時間順序連邊)
#include<bits/stdc++.h> #define maxn 100010 using namespace std; int n,m,ans,fa[maxn]; struct node{ int x,y,t; }data[maxn]; bool cmp(node a,node b){ return a.t<b.t; } int find(int x){ if(x!=fa[x]) fa[x]=find(fa[x]); return fa[x]; } void merge(int x,int y){ int fa1=find(x),fa2=find(y); if(fa1==fa2) return; else{ fa[fa1]=fa2; ++ans; } } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) fa[i]=i; for(int i=1;i<=m;++i) scanf("%d%d%d",&data[i].x,&data[i].y,&data[i].t); sort(data+1,data+m+1,cmp); for(int i=1;i<=m;++i){ merge(data[i].x,data[i].y); if(ans==n-1){ printf("%d",data[i].t); return 0; } } printf("-1"); return 0; }
關於連通塊:
並查集可以做連通塊的題目
目前連通塊的個數=最開始的連通塊數-並和次數
注意合併次數指兩個不同的集合合併
例題【洛谷P1197 星球大戰】(因為要減邊所以反過來做+求連通塊個數)
#include<bits/stdc++.h> #define maxn 400010 using namespace std; int n,m,k,ans,fa[maxn],pl[maxn]; bool v[maxn]; int head[maxn],edge_num; struct{ int to,nxt; }edge[maxn]; void add_edge(int from,int to){ edge[++edge_num].nxt=head[from]; edge[edge_num].to=to; head[from]=edge_num; } int find(int x){ if(x!=fa[x]) fa[x]=find(fa[x]); return fa[x]; } void merge(int x,int y){ int fa1=find(x),fa2=find(y); if(fa1==fa2) return; else fa[fa1]=fa2,--ans; } void Build(){ for(int i=1;i<=n;++i) fa[i]=i; ans=n; for(int i=1;i<=n;++i) if(!v[i]) for(int j=head[i];j;j=edge[j].nxt){ int to=edge[j].to; if(!v[to]) merge(i,to); } } void Solve(int x){ if(x==0) return; v[pl[x]]=false; for(int i=head[pl[x]];i;i=edge[i].nxt){ int to=edge[i].to; if(!v[to]) merge(pl[x],to); } int res=ans; Solve(x-1); printf("%d\n",res-x+1); } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=m;++i){ int u,v; scanf("%d%d",&u,&v); add_edge(u+1,v+1); add_edge(v+1,u+1); } scanf("%d",&k); for(int i=1;i<=k;++i) scanf("%d",&pl[i]),pl[i]+=1,v[pl[i]]=true; Build(); int res=ans; Solve(k); printf("%d\n",res-k); return 0; }
關於帶權並查集:
這些權值的處理可以根據並查集路徑壓縮的特質來處理
若是集合的權值,可以直接新建陣列仿照fa[ ]的合併進而合併即可
另外有一點特別神奇,點的權值處理可以在路徑壓縮是進行處理
例題【洛谷P1196 銀河英雄傳說】
每在兩列合併時,都需要將該列所有的戰艦更新位置嗎?
這樣的話不就和最開始暴力“並查集”一樣了嗎。
我們可以發現,每列的戰艦x的位置可以通過已知fa[x]更新出來
loc[x]表示x戰艦的位置
只要進行一下兩種操作即可
1.每次合併,先把i列頭頭的位置更新出來,先處理i列頭頭的位置,i列頭頭的位置=j列的長度+1
2.我們規定只在路徑壓縮的時候更新該戰艦的位置,那麼第i列第x個戰艦的真實位置即loc[x]=loc[fa[x]]+loc[x]-1
這裡有點繞,理一理,當集合更新的後,loc[x]還是以fa[x]為艦首時的位置,而因為遞迴的緣故loc[fa[x]]已經更新完畢,所以loc[fa[x]]是正確的,loc[x]目前看來是錯誤的
而更新完loc[x]後,fa[x]變為j列的艦頭,loc[j列艦頭]=1,所以以後更新到x也不用擔心loc[ ]值的改動
為了使用fa[x]來處理loc所以並查集部分有變化
注意這題中合併要按順序,是i列放在j列後面
#include<bits/stdc++.h> #define maxn 30010 using namespace std; int T,x,y; char ch; int fa[maxn],siz[maxn],loc[maxn]; int find(int x){ if(fa[x]==x) return x; int fath=find(fa[x]); loc[x]=loc[fa[x]]+loc[x]-1; fa[x]=fath; return fa[x]; } int main(){ scanf("%d",&T); for(int i=1;i<=maxn;++i) fa[i]=i,siz[i]=1,loc[i]=1; for(int i=1;i<=T;++i){ cin>>ch>>x>>y; int fa1=find(x),fa2=find(y); if(ch=='M') fa[fa1]=fa2,loc[fa1]=siz[fa2]+1,siz[fa2]+=siz[fa1]; else{ if(fa1!=fa2) printf("-1\n"); else printf("%d\n",abs(loc[y]-loc[x])-1); } } return 0; }
關於反集:
就是題目給定的規則,要好幾個並查集一起操作
例題1【洛谷P1892 團伙】
關係整理(由於題目要求所以只需要連出友好線即可):
若p---敵--->q
p的敵人---友--->q的敵人
p的敵人---友--->q的朋友
q的敵人---友--->p的朋友
q的敵人---友--->p的敵人
若p---友--->q
p的朋友---友--->q的朋友
q的朋友---友--->p的朋友
把x+n的點當作x的敵人,那麼n+1~n*2都是敵人集合
#include<bits/stdc++.h> using namespace std;
int father[10010]; int n,m,p,q,ans; char ch;
int find(int x) { if(father[x]!=x) father[x]=find(father[x]); return father[x]; }
int main(){ cin>>n>>m; for(int i=1;i<=2*n;i++) father[i]=i; for(int i=1;i<=m;i++){ cin>>ch>>p>>q; if(ch=='F') father[find(p)]=find(q); else{ father[find(n+p)]=find(q); father[find(n+q)]=find(p); } } for(int i=1;i<=n;i++) if(father[i]==i) ans++; cout<<ans; return 0; }
例題2【洛谷P2024 食物鏈】
類似於例1,這次是3個集合互連。
#include<bits/stdc++.h> #define maxn 100010 using namespace std; int n,m,fa[maxn*3],ans=0; int find(int x){ if(fa[x]!=x) fa[x]=find(fa[x]); return fa[x]; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=3*n;++i) fa[i]=i; for(int i=1;i<=m;++i){ int opt,x,y; scanf("%d%d%d",&opt,&x,&y); if(x>n||y>n){++ans;continue;} int fa1=find(x),fa2=find(y),fa3=find(x+n),fa4=find(y+n),fa5=find(x+n+n),fa6=find(y+n+n); //12同類 34獵物 56天敵 if(opt==1){ if(fa1==fa4||fa1==fa6) ++ans; else fa[fa1]=fa[fa2],fa[fa3]=fa[fa4],fa[fa5]=fa[fa6]; } else{ if(x==y){++ans;continue;} if(fa1==fa2||fa4==fa1) ++ans; else fa[fa6]=fa[fa1],fa[fa3]=fa[fa2],fa[fa5]=fa[fa4]; } } printf("%d",ans); }