1. 程式人生 > 其它 >並查集與其優化(啟發式合併、壓縮路徑)

並查集與其優化(啟發式合併、壓縮路徑)

我們哭著降臨世界,卻可以笑著走向永恆。

並查集

給定 \(n\) 個集合 \(\{ a_1 \}\)\(,···,\)\(\{ a_n \}\),有 \(m\) 個分別為「合併集合」和「查詢兩個元素是否屬於同於同一個集合」的操作。

洛谷 P3367【模板】 並查集

樸素並查集

考慮使用樹狀結構儲存每個集合,元素 \(a_i\) 對應節點 \(u_i\)

  • 合併集合

    ​ 將一個集合對應的樹的根節點的父親設定為另一個集合的樹的根節點。

  • 查詢兩個元素是否屬於同一個集合

    ​ 查詢兩個元素對應的節點是否有同一個根節點。

複雜度

  • 查詢根節點:\(O(n)\)
  • 合併集合:兩次查詢根節點,加邊 \(O(n)\)
  • 查詢兩個元素是否屬於同一集合:兩次查詢根節點 \(O(n)\)

演算法的瓶頸在於查詢根節點的開銷過高,考慮如何儘可能的減少每個節點到根節點的路徑長度。

優化:啟發式合併(按秩合併)

對於每個節點 \(u_i\) 維護一個子樹的大小 \(size_i\),此時考慮合併兩個元素 \(a_x\) , \(a_y\) 所在的集合。

  • 找到 \(u_x\) 的根節點 \(a_p\)\(u_y\) 的根結點 \(a_q\),此時兩個集合的大小分別為 \(size_p\),\(size_q\)
  • 如果\(size_q<size_p\),那麼 \(u_p\)\(u_q\) 的父親;否則,\(u_q\)\(u_p\) 父親。

由於 \(size_p>size_q\)

,因此 \(size_p+size_q>2 \times size_q\),也就是說在找尋根節點的過程中,每經過一條邊都會至少使得子樹的大小翻倍,因此查詢根節點的複雜度為 \(O(log(n))\)。 同時「合併集合」和「查詢兩個元素」是否同一個集合都只依賴查詢根節點的操作,因此單詞操作的複雜度為 \(O(\log(n))\)

優化:壓縮路徑

對於每個集合對應的樹,我們可以將任意一個子樹的父親設定為根節點而不影響整體答案的合法性。在最優的情況下,樹退化成菊花圖,查詢根節點的開銷為 \(O(1)\)

考慮在每次查詢根節點的過程中,將其所有祖先的父親直接設定為根節點。 單次查詢根節點的均攤開銷為 \(O(\alpha(n))\)

,其中 \(\alpha\) 在正常的資料範圍內幾乎為常數,因此在路徑壓縮下「合併集合」和「查詢兩個節點 是否屬於同一個集合」可以視為常數。

程式碼

#include<bits/stdc++.h>
#define N 10010
using namespace std;
int f[N],n,m; 
inline int FIND(int);

int main()
{
	scanf("%d %d",&n,&m);
	memset(f,0,sizeof(f));
	for(int i=1;i<=n;i++)
		f[i]=i;
	for(int i=1,x,y,z;i<=m;i++)
	{
		scanf("%d %d %d",&z,&x,&y);
		if(z==1)
			f[FIND(x)]=FIND(y);
		else
		{
			if(FIND(x)==FIND(y))
				putchar('Y');
			else
				putchar('N');
			putchar('\n');
		}
	}
	return 0;
}


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

練習

洛谷 P1955 [NOI2015] 程式自動分析

離散化後直接並查集處理。

程式碼

#include<bits/stdc++.h>
#define N 100005 
#define int long long
using namespace std;

stack<int>si,sj,snull;
map<int,int>mq;
int t,n,dcnt,f[N<<1];
inline int ADD(int);
inline int FIND(int);

signed main()
{
	scanf("%lld",&t);
	while(t--)
	{
		memset(f,0,sizeof(f));
		scanf("%lld",&n);
		for(int i=1;i<=n*2;i++)
			f[i]=i;
		mq.clear();
		si=snull,sj=snull,dcnt=0;
		for(int l=1,i,j,e;l<=n;l++)
		{	
			scanf("%lld %lld %lld",&i,&j,&e);
			int signi=ADD(i),signj=ADD(j);
			if(e)
				f[FIND(signi)]=FIND(signj);
			else
				si.push(signi),sj.push(signj);
		}
		while(!si.empty())
		{
			if(FIND(si.top())==FIND(sj.top()))
				break;
			si.pop(),sj.pop();
		}
		if(si.empty())
			printf("YES");
		else
			printf("NO");
		putchar('\n');
	}	
	return 0;
}

inline int ADD(int x)
{
	if(mq[x])
		return mq[x];
	dcnt++;
	mq[x]=dcnt;
	return dcnt;
}

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

寫於 2021年7月6日 在 焦作一中 集訓中