並查集の進階用法

2023-05-01 21:00:18

普通並查集

我們在處理問題的時候,可能會遇到一些需要維護每個元素所在的集合的問題,而並查集卻恰好完美解決了這個問題。

對於普通的並查集,他支援的操作比較少,只有合併和查詢,合併是指把兩個集合合併成一個,而查詢是詢問兩個元素是否在同一集合內;對於這兩種操作,我們可以用一個陣列 \(f\) 來存放當前點所屬的集合的代表元素編號,例如,\(f[i]=3\) 表示的就是第 \(i\) 個元素屬於編號為 \(3\) 的元素所在的集合,那麼你可能會問了,用了這個陣列,查詢是好辦了,我們可以直接寫一個 find 函數來查詢:

inline void fid(int x){return f[x]==x?x:fid(f[x]);}
//如果當前的父節點是自己就返回自己,反之返回代表元素的集合所在的代表元素編號依次直到f[x]==x;

當然我們在使用的時候發現這個 fid 如果一直往後遞迴會很費時間,所以我們用可以用以下的程式碼來優化一下:

inline void fid(int x){return f[x]==x?x:f[x]=fid(f[x]);}

這種優化也叫路徑壓縮,我個人的理解就是加了個記憶化。

那合併呢,咋搞?

假設我們需要把 \(x\)\(y\) 所屬的兩個集合合併。

法一:直接暴力把所有的點遍歷一遍,把所有的屬於 \(y\) 所屬的集合的 \(f\) 陣列給暴力修改成 \(x\) 所屬的集合的代表元素編號。複雜度 \(O(n)\)

法二:只修改 \(f[y]\) 的值,讓他所屬的集合代表元素的編號修改為 \(x\) 所屬的集合代表元素的編號,後面在遇到和 \(y\) 原先屬於同一集合的元素時再更新。複雜度 \(O(1)\)

很顯然第一個會 T 飛,第二個複雜度很不錯但是如何來實現呢?

我們之前記錄的 \(f\) 陣列是有大用的,配合我們的 fid 函數使我們可以輕而易舉的完成法二,當你把 \(y\) 所屬的集合的代表元素編號改為 \(x\) 所屬的集合的代表元素編號,這樣我們在後面的時候再查到所屬集合為 \(y\) 所屬的集合的元素時,我們就會因為 fid 函數一步一步遞迴更新 \(f\) 陣列的值來實現法二操作了。

但是當 \(f[y]=k\)\(f[k]=k\) 的時候怎麼辦,那樣不就沒法更新了嗎?

所以我們在進行修改的時候改的其實並不是把 \(f[y]=f[x]\),而是 \(f[fid(y)]=f[fid(x)]\),直接從根源合併,這樣就可以做到合併操作了。

P3367 【模板】並查集

code:

#include<bits/stdc++.h>
using namespace std;
int n,m,f[100010];
inline int fid(int x)
{
	if(f[x]==x)return x;
	else return f[x]=fid(f[x]);
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	  f[i]=i; 
	for(int i=1;i<=m;i++)
	{
		int op,x,y;
		cin>>op>>x>>y;
		if(op==1)
		{
			int xx=fid(x);
			int yy=fid(y);
			f[xx]=f[yy];
		}
		else if(op==2)
		{
			int xx=fid(x);
			int yy=fid(y);
			if(xx==yy)
			  cout<<"Y"<<endl;
			else cout<<"N"<<endl;
		}
	}
	return 0;
}

擴充套件域並查集

我也不知道是不是叫這個名字,但是是解決 \(n\) 個點有 \(m\) 對關係,把 \(n\) 個節點放入兩個集合裡,要求每對存在關係的兩個節點不能放在同一個集合這類的問題的一個並查集。

結合一道題目來講一下具體怎麼用:

P1892 [BOI2003]團伙

一個人的朋友的朋友是朋友
一個人的敵人的敵人是朋友

把一個人 \(i\) 劈成兩半,一好 \(i\) 一壞 \(i+n\),當一個人 \(j\) 和他是朋友的時候,和好的是同一個集合,合併 \(j\)\(i\);如果是敵人,就和壞的是同一集合,合併 \(j\)\(i+n\)\(j+n\)\(i\)

最後你就會發現,和 \(i\) 為敵的都和 \(i+n\) 在一個集合裡,和 \(i\) 處於不同集合,這樣我們也就完成了題目的要求。

如果 \(a\)\(b\) 是敵人,合併 \(n+b\)\(a\)\(n+a\)\(b\)

如果 \(c\)\(a\) 是敵人,合併 \(n+c\)\(a\)\(n+a\)\(c\)

那麼 \(b\)\(c\) 就並在一起了

code:

#include<bits/stdc++.h>
using namespace std;
int n,m,f[15000],bd[15000],ans=0;
int fid(int x)
{
	if(f[x]==x)return f[x];
	else return f[x]=fid(f[x]);
}
void cun(int x,int y)
{
	int xx=fid(x);
	int yy=fid(y);
	if(xx!=yy)f[yy]=xx;	
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n*2;i++)
	f[i]=i;
	for(int i=1;i<=m;i++)
	{
		int b,c;
		char a;
		cin>>a>>b>>c;
		if(a=='F')
		cun(b,c);
		if(a=='E')
		{
			cun(b+n,c);
			cun(c+n,b);
		}
	}
	for(int i=1;i<=n;i++)
	{
	   	int t=fid(i);
	   	if(bd[t]==0)
	   	  bd[t]=1,ans++;
	}
	cout<<ans<<endl;
	return 0;
}

P2024 [NOI2001] 食物鏈

這個題和上面的有什麼區別?

上一個題目裡一個人只需要劈兩半,因為 A 可以和 B 為敵,B 和 A 為敵。

但是這個不可以,A 可以吃 B,但這樣 B 不能吃 A。

所以我們一個人劈三半!A 吃 B,B 吃 C,C 吃 A!

在合併的時候,我們就正常每一個集合都合併,對於 A 吃 B 之類的操作,依次標記即可。

code:

#include<bits/stdc++.h>
#define N 10001000
using namespace std;
int n,k,f[N],ans,a,b,c;
inline int fid(int x){return f[x]==x?x:f[x]=fid(f[x]);} 
signed main()
{
	cin>>n>>k;
	for(int i=1;i<=3*n;i++)f[i]=i;
	for(int i=1;i<=k;i++)
	{
		cin>>a>>b>>c;
		if(b>n||c>n){ans++;continue;}
		if(a==1)
		{
			if(fid(b+n)==fid(c)||fid(c+n)==fid(b)){ans++;continue;}
			f[fid(b)]=fid(c);
			f[fid(b+n)]=fid(c+n);
			f[fid(b+n+n)]=fid(c+n+n);
		}
		if(a==2)
		{
			if(fid(b)==fid(c)||fid(b)==fid(c+n)){ans++;continue;}
			f[fid(b+n)]=fid(c);
			f[fid(b+n+n)]=fid(c+n);
			f[fid(b)]=fid(c+n+n); 
		}
	}
	cout<<ans<<endl;
	return 0;
}

加權並查集

我記得這東西還叫帶權並查集。

這個東西和普通的並查集有什麼區別嘞?

他可以查詢兩個點之間的距離。

然後就沒有別的了。。。。

我也不是很明白這個東西是怎麼實現的,但是可以講一下下面的這個題目(以後一定來填坑)

P1196 [NOI2002] 銀河英雄傳說

這個題目的詢問不太一樣:詢問兩個戰艦之間的戰艦數量。

我們開一個陣列 \(f\) 來存放當前戰艦到這一列盡頭之間的戰艦數量,然後用 \(fa\) 來表示當前的點所屬的集合的代表元素的編號,其次,我們還需要一個 \(num\) 陣列來存放當前集合,也就是這一列的戰艦總數。

我們和普通的並查集相比,其實只有兩點不同:

一是我們的 fid 函數,在路徑亞索壓縮的時候順便把 \(f\) 陣列也要處理一下,比如說,在路徑壓縮的時候我們是相當於把這個點接到更新後的點所在列的後面,所以我們要這樣改:

inline int fid(int x)
{
	if(fa[x]==x)return x;//如果要是相等直接返回 
	int ff=fid(fa[x]);//更新後的代表元素編號 
	f[x]+=f[fa[x]];//累加求和 
	return fa[x]=ff;//返回新的代表元素編號 
}

第二就是合併操作的改變,同樣我們除了改變 \(fa\) 以外還要修改 \(f\) 陣列,所以我們這樣合併:

f[xx]+=num[yy];//把xx一列接到yy一列的後面,所以直接加上num[yy] 
fa[xx]=yy;//更新fa 
num[yy]+=num[xx];//累加戰艦數量 
num[xx]=0;//清零 

其實我覺得帶權並查集都可以把集合給相成拍一列,因為這樣的話能比較好理解和實現。

code:

#include<bits/stdc++.h>
#define int long long
#define N 1000100
using namespace std;
int n,x,y,num[N],f[N],fa[N];
inline int fid(int x)
{
	if(fa[x]==x)return x;//如果要是相等直接返回 
	int ff=fid(fa[x]);//更新後的代表元素編號 
	f[x]+=f[fa[x]];//累加求和 
	return fa[x]=ff;//返回新的代表元素編號 
}
signed main()
{
	for(int i=1;i<=30000;i++)
	  fa[i]=i,f[i]=0,num[i]=1;
	cin>>n;
	while(n--)
	{
		char c;
		cin>>c>>x>>y;
		int xx=fid(x);
		int yy=fid(y);
		if(c=='M')
		{
			f[xx]+=num[yy];//把xx一列接到yy一列的後面,所以直接加上num[yy] 
			fa[xx]=yy;//更新fa 
			num[yy]+=num[xx];//累加戰艦數量 
			num[xx]=0;//清零 
		}
		else
		{
			if(xx!=yy)cout<<"-1"<<endl;
			else cout<<abs(f[x]-f[y])-1<<endl;
		}
	}
	return 0;
}

可持久化並查集

之前的並查集都弱爆了,這個才是最強並查集。

前置知識:主席樹,並查集

md沒看懂,先咕了