[算法總結]並查集



一、關於並查集

1. 定義

並查集(Disjoint-Set)是一種可以動態維護若干個不重疊的集合,並支持合並查詢兩種操作的一種數據結構。

2. 基本操作

1. 合並(Union/Merge)[1]合並兩個集合。
2. 查詢(Find/Get):查詢元素所屬集合。
實際操作時,我們會使用一個點來代表整個集合,即一個元素的根結點(可以理解為父親)。

3. 具體實現

我們建立一個數組fa[ ]pre[ ]表示一個並查集,fa[i]表示i的父節點。
初始化:每一個點都是一個集合,因此自己的父節點就是自己fa[i]=i
查詢:每一個節點不斷尋找自己的父節點,若此時自己的父節點就是自己,那么該點為集合的根結點,返回該點。
修改:合並兩個集合只需要合並兩個集合的根結點,即fa[RootA]=RootB,其中RootA,RootB是兩個元素的根結點。

路徑壓縮:
實際上,我們在查詢過程中只關心根結點是什么,並不關心這棵樹的形態(有一些題除外)。因此我們可以在查詢操作的時候將訪問過的每個點都指向樹根,這樣的方法叫做路徑壓縮,單次操作復雜度為\(O(logN)\)
結合下圖食用更好(圖為狀態壓縮的過程):
圖片3.png


二、代碼實現

初始化的模板:

for(int i=1;i<=n;i++) pre[i]=i;

查詢的模板(含路徑壓縮):

int Find(int x){
    if(x==pre[x]) return x;
    return pre[x]=Find(pre[x]);
}

合並的模板:

void merge(int x,int y){
    int fx=Find(x),fy=Find(y);
    if(fx!=fy) pre[fx]=fy;
}
//主函數內
merge(a,b);

三、一些例題

例1:P1551 親戚

模板題。這里就不放代碼了...
同樣是模板題:P2814 家譜,建議使用map(STL)。

例2:P1536 村村通

求出讓所有道路聯通時要建多少條道路。
我們先把已經建好的道路的點合並,如果還需要建道路,那么所有點組成的集合數量一定大於1。
我們只要查詢是否單個點組成了一個集合,即詢問一個點的父節點是否為本身。
值得注意的是最終合並為一個集合中的根結點的父節點也是其本身,我們最后輸出答案的時候要減掉。
Code:

#include <bits/stdc++.h>
using namespace std;
int pre[1000001],n,m,ans;
inline int Find(int x){
	return pre[x]==x?x:pre[x]=Find(pre[x]);
}
inline void Union(int x, int y){
	int fx=Find(x),fy=Find(y);
	if(fx!=fy) pre[fx]=fy;
}
int main()
{
    while(scanf("%d",&n)&&n){
        ans=0;
        scanf("%d", &m);
        for(int i=1;i<=n;i++) pre[i]=i;
        for(int i=1,x,y;i<=m;i++){
            scanf("%d%d",&x,&y);
            Union(x,y); 
        }
        for(int i=1;i<=n;i++){
            if(Find(i)==i) ans++;
        }
        printf("%d\n",ans-1);
    }
    return 0;
}

例3:P1396 營救

FBI!Open UP!看到求最大值的最小我們知道一定會用二分解決。
順着這個思路走,我們可以二分這個擁擠度,在判斷這個擁擠度是否可行時,把所有擁擠度大於mid的邊都去掉,最后並查集判斷s點與t點是否聯通即可。
Code:

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define N 50050
using namespace std;
int l=INF,r=-1,n,m,s,t,ans;
int pre[N],x[N],y[N],cost[N];
inline int find(int x){return x==pre[x]?x:pre[x]=find(pre[x]);}
inline int check(int mid){
	for(int i=1;i<=n;i++) pre[i]=i;
	for(int i=1;i<=m;i++){
		if(cost[i]>mid) continue;
		int fx=find(x[i]),fy=find(y[i]);
		if(fx!=fy) pre[fx]=fy;
	}
	if(find(s)==find(t)) return 1;
	return 0;
}
int main()
{
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&x[i],&y[i],&cost[i]);
		l=min(l,cost[i]);r=max(r,cost[i]);
	}
	while(l<=r){
		int mid=(l+r)>>1;
		if(check(mid)){
			r=mid-1;
			ans=mid;
		}
		else l=mid+1;
	}
	printf("%d",ans);
	return 0;
}

例4:P1621 集合

把所有質因數大於p的數都求出來合並,最后並查集求解。

#include<bits/stdc++.h>
#define N 100010
using namespace std;
int a,b,cmp,ans,cnt;
int pre[N],notp[N],p[N];
int Find(int x){
    if(x==pre[x]) return x;
    return pre[x]=Find(pre[x]);
}
inline int Union(int x,int y){
	int fx=Find(x),fy=Find(y);
	if(fx!=fy) pre[fx]=fy;
}
inline void E_prime(){
	notp[1]=1;
	for(int i=2;i<=b;i++){
		if(notp[i]) continue;
		for(int j=i*2;j<=b;j+=i){
			notp[j]=1;
		}
	}
	for(int i=cmp;i<=b;i++)
		if(not notp[i]) p[++cnt]=i;
}
int main()
{
	scanf("%d%d%d",&a,&b,&cmp);
	for(int i=a;i<=b;i++) pre[i]=i;
	E_prime();
	for(int i=1;i<=cnt;i++)
		for(int j=2;j*p[i]<=b;j++)
			if(j*p[i]>=a&&(j-1)*p[i]>=a)
				Union(p[i]*j,p[i]*(j-1));
	for(int i=a;i<=b;i++)
		if(pre[i]==i) ans++;
	printf("%d",ans);
	return 0;
}

例5:P4185 [USACO18JAN]MooTube

把K和邊權從大往小排序,這樣大的K也滿足小的K,避免了重復搜索。
Code:

#include<bits/stdc++.h>
#define N 300010
using namespace std;
int n,q,fa[N],size[N],ans[N];
inline int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
inline void Union(int x,int y){
    int fx=find(x),fy=find(y);
    if(fx==fy) return;
    size[fx]+=size[fy],fa[fy]=fx;
}
struct edge{
	int u,v,r;
}p[N];
struct node{
	int k,v,id;
}ask[N];
inline int CMP(edge a,edge b){return a.r>b.r;}
inline int cmp(node a,node b){return a.k>b.k;}
int main()
{
	scanf("%d%d",&n,&q);
    for(int i=1;i<n;i++)
		scanf("%d%d%d",&p[i].u,&p[i].v,&p[i].r);
    for(int i=1;i<=q;i++)
		scanf("%d%d",&ask[i].k,&ask[i].v),ask[i].id=i;
	for(int i=1;i<=n;i++){fa[i]=i;size[i]=1;}
    sort(p+1,p+n,CMP);
    sort(ask+1,ask+q+1,cmp);
    int pos=1;
    for(int i=1;i<=q;i++){
        while(pos<n&&p[pos].r>=ask[i].k){
        	Union(p[pos].u,p[pos].v);
			pos++;
		}
        ans[ask[i].id]=size[find(ask[i].v)]-1;
    }
    for(int i=1;i<=q;i++)
		printf("%d\n",ans[i]);
    return 0;
}

例6:P1197 [JSOI2008]星球大戰

題目要求求出每次打擊后圖中連通塊的個數。並查集可以進行合並操作,卻不能進行分離操作,所以我們倒序處理。先聯通沒有被炸的點並統計出此時連通塊的個數,接着不斷合並被炸的點並統計此時的連通塊個數。
Code:

#include<bits/stdc++.h>
#define N 400010
using namespace std;
int n,m,k,pre[N],first[N],nxt[N],go[N];
int poi[N],off[N],tot,from[N],sum,ans[N];
inline void add_edge(int u,int v){
	nxt[++tot]=first[u];
    first[u]=tot;
    go[tot]=v;
    from[tot]=u;
}
inline int Find(int x){
	return x==pre[x]?x:pre[x]=Find(pre[x]);
}
inline void Union(int x,int y){
    int fx=Find(x),fy=Find(y);
    if(fx!=fy) pre[fx]=fy;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) pre[i]=i;
    for(int i=1,x,y;i<=m;i++){
        scanf("%d%d",&x,&y);
        add_edge(x,y);
		add_edge(y,x);
    }
    scanf("%d",&k);
    for(int i=1;i<=k;i++){
        scanf("%d",&poi[i]);
        off[poi[i]]=1;//標記 
    }
    sum=n-k;//剩余的點 
    for(int i=1;i<=m<<1;i++)//雙向邊 
        if(!off[from[i]]&&!off[go[i]])//兩個點都沒被炸 
            if(Find(from[i])!=Find(go[i]))
				sum--,Union(from[i],go[i]);//合並,更新連通塊個數 
    ans[k+1]=sum;//最后一個答案 
    for(int i=k;i>=1;i--){
        sum++;//恢復這個點后自己也算一個連通塊,因此個數要+1 
        off[poi[i]]=0;//恢復 
        for(int e=first[poi[i]];e;e=nxt[e]){
            int v=go[e];
			if(!off[v]&&Find(poi[i])!=Find(v)){
                Union(poi[i],v);//合並 
				sum--; 
            }
        }
        ans[i]=sum;
    }
    for(int i=1;i<=k+1;i++)
    	printf("%d\n",ans[i]);
    return 0;
}

例7:bzoj2054瘋狂的饅頭

2054_1.jpg
我們發現最后滿頭染上的顏色只與他最后一次被染上的顏色有關,因此我們倒着處理,這時我們需要維護已經染好的饅頭不會再被染色。從區間的左向右依次掃描,尋找這個點的父親,此時這個父親就是還沒有被填上顏色的節點(如果這個父親節點仍在區間里),我們把這個點染色並標記這個點指向下一個節點。簡明的來講,就是當我們想對一個點染色時,這個點會把我們帶到另一個沒有染色的點來染色。
圖片4.png

#include<bits/stdc++.h>//每次掃描的元素被染色后指向其沒有被染色的饅頭 
#define N 1000010      //路徑壓縮以節省時間 
#define ll long long
using namespace std;
int n,m,p,q;
int pre[N],ans[N];
inline int Find(int x){
	return (pre[x]==x)? x:pre[x]=Find(pre[x]);
}
inline void Dye_mantou(ll l,ll r,int color)
{
	for(int i=Find(l);i<=r;i=Find(i)){			//方向指向數組末尾 
		ans[i]=color;
		pre[i]=i+1;
	}
}
int main()
{
	scanf("%d%d%d%d",&n,&m,&p,&q);
	for(int i=1;i<=n+1;i++) pre[i]=i;
	for(int i=m;i>=1;i--){
		ll l=(i*p+q)%n+1;
		ll r=(i*q+p)%n+1;
		if(l>r) swap(l,r);
		Dye_mantou(l,r,i);
	}
	for(int i=1;i<=n;i++)
		printf("%d\n",ans[i]);
	return 0;
}

例8:P2294 [HNOI2005]狡猾的商人

帶權並查集。通過並查集建立月份與月份的關系。
Code:

#include<bits/stdc++.h>
#define N 100010
using namespace std;
int n,m,w;
int pre[N],d[N];
int Find(int x){
    if(pre[x]==x) return x;
    int temp=Find(pre[x]);
    d[x]+=d[pre[x]];
    return pre[x]=temp;
}
int Union(int x,int y,int v)
{
    int fx=Find(x);
	int fy=Find(y);
    if(fx==fy)
		return d[x]-d[y]==v;
    if(fx<fy){
        pre[fx]=fy;
		d[fx]=d[y]+v-d[x];
        return 1;
    } 
    else{
        pre[fy]=fx;
		d[fy]=d[x]-v-d[y];
        return 1;
    }
}
int main()
{
    scanf("%d",&w);
    while(w--)
    {
        scanf("%d%d",&n,&m);
        int flag=1;
        memset(d,0,sizeof(d));
        for(int i=0;i<=n;i++) pre[i]=i;
        for(int i=1,s,t,v;i<=m;i++){
            scanf("%d%d%d",&s,&t,&v);
            if(flag==0) continue;
            if(!Union(s-1,t,v)) flag=0;//包含s月,所以往前一個月為s-1
        }
        if(flag) printf("true\n");
        else printf("false\n");
    }
    return 0;
}

例9:P1892 [BOI2003]團伙

題目的兩種關系已經說得很明確了,我們將朋友關系的兩個人合並。對於是敵人關系的兩個人,由於敵人的敵人是我的朋友,所以我們可以建立一個自己虛擬的敵人再與對方形成朋友關系。
Code:

#include<bits/stdc++.h>
#define N 6000
using namespace std;
int n,m,ans,pre[N];
char ch;
int Find(int x){
	return (x==pre[x])? x:pre[x]=Find(pre[x]);
}
inline void Union(int x,int y){
	int fx=Find(x);
	int fy=Find(y);
	pre[fx]=fy;
	return;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=2*n;i++) pre[i]=i;
	for(int i=1,u,v;i<=m;i++){
		cin>>ch>>u>>v;
		if(ch=='F') Union(u,v);
		if(ch=='E'){
			pre[Find(u+n)]=Find(v);
			pre[Find(v+n)]=Find(u);
		}
	}
	for(int i=1;i<=n;i++)
		if(pre[i]==i) ans++;
	printf("%d",ans);
	return 0;
}

pic.png


  1. 怎么起名都無所謂啦 ↩︎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM