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


    并查集

    给定 (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日 在 焦作一中 集训中

  • 相关阅读:
    sqlserver建立临时表
    动态引用WebService
    技术的力量:30分钟的动画片和《彗星撞地球》超炫的动画 仅64K
    sqlserver2005新功能函数
    使用面向对象的、完整的单点登录功能
    asp.net上传功能(单文件,多文件,自定义生成缩略图,水印)
    C#对字符和文件的加密解密类
    JavaScript中setInterval函数应用常见问题之一(第一个参数不加引号与加引号的区别)
    JavaScript表格隔行换色悬停高亮
    Javascript模拟c#中arraylist操作(学习分享)
  • 原文地址:https://www.cnblogs.com/Dr-Albert-Wensley/p/union-find_disjoint_sets.html
Copyright © 2020-2023  润新知