并查集与其优化(启发式合并、压缩路径)

并查集

给定 (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 imes 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('
');
		}
	}
	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('
');
	}	
	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日 在 焦作一中 集训中

原文地址:https://www.cnblogs.com/Dr-Albert-Wensley/p/union-find_disjoint_sets.html