并查集—进阶

目录

例题1

题目

题解

例题2

题目

题解

T1~2 Summarize

例题3

题目

题解

代码

T1~2 Summarize

例题4

题目

题解

例题5

题目

题解

代码

例题6

题目

题解

T4~6 Summarize


例题1

题目

给一个无向图,求其中的连通块。

题解

并查集就是一个集合,并且以fa[x]为集合代表。
对于这题,如果x,y有连边,把它们放入一个集合中。


例题2

题目

NOI 2015程序自动分析

题解

如果ai=aj则把i和j放入一个集合,
再处理完所有的相等关系后,把不等关系当作检验条件。
1<=x<=1e9当然要离散化。

 

T1~2 Summarize

并查集能在一张图中维护节点之间的连通性,擅长维护许多具有传递性的关系。所谓传递性,即A和B有关系,B和C有关系,那么A和C也有一定关系。

 

例题3

题目

poj1456 Supermarket

题解

一个贪心策略是用一个二叉堆维护暂定所卖的商品,按过期顺序遍历,如果有新的利润更大的商品,则将其替换。另一个贪心策略:对于一个未选的利润最大商品,我们安排它在尽量靠近过期时间卖掉。
对于前一个贪心具体讲解及实现,这里不再是重点。我们谈谈第二种。
如何做到快速求到距离第x天最近的一天呢?
我们用并查集维护。
对于一天d,我们给它设立一个节点。如果d被使用了,那么我们让fa[d]=fa[d-1]。
大概就是这个意思。
我们给每天都开设一个节点,d所处的集合的代表(定义为集合中最小元素的值),就是集合中任意一天的最近可用的一天。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=10010;

int n;

struct U{int p,d;}a[maxn];
bool cmp(U u1,U u2)
{
	return u1.p>u2.p;
}

int fa[maxn];
int find_fa(int x)
{
	if(x==fa[x]) return x;
	return fa[x]=find_fa(fa[x]);
}

int main()
{
	while(scanf("%d",&n)!=EOF)
	{
		int mxd=0;
		for(int i=1;i<=n;i++)
		{
			scanf("%d%d",&a[i].p,&a[i].d);
			mxd=max(mxd,a[i].d);
		}
		sort(a+1,a+n+1,cmp);
		for(int i=1;i<=mxd;i++) fa[i]=i;
		int ans=0;
		for(int i=1;i<=n;i++)
		{
			int day=find_fa(a[i].d);
			if(day==0) continue;
			ans+=a[i].p;
			fa[day]=find_fa(day-1);
		}
		printf("%d\n",ans);
	}
	return 0;
}

T1~2 Summarize

这其实利用了并查集路径压缩的原理,使得从第i天免去了遍历i-1,i-2,…,才找到第j天的麻烦。
这道题也利用了这一点,这是一个更加浅显的直接利用路径压缩的例子。

 

例题4

题目

NOI2002 银河英雄传说

题解

《并查集-应用》中已有详解,这里不再赘述。

 

 

在讲下一个例题之前,我要介绍一下《并查集-进阶》的重点知识。既然坚持看到了这里,不往下看半途而废就是失败!!!
并查集的两大操作“扩展域”和“边带权”,其中“边带权”在《并查集-应用》中已经有所介绍,那个所谓的“种类并查集”也可以归为“边带权并查集”。

先讲讲“边带权并查集”吧。并查集可以看作是一个森林,这就涉及到树论了。树中最常见的就是边权,点权,我们把那一套东西搬到并查集中来。
尽管两个节点同属于一个集合,如果做到这里,它们之间的关系并没有得到展现。为了表现一个集合中元素与元素间的关系,我们加入了边权,进而得到点权,不同的点权有不同的含义,这就使得一个集合内的元素的关系得以充分的展现。

扩展域”类似于分层图,也就是拆点思想。把一个点拆分成多个点,每个点表示不同意义。在自此基础连边,就能用衍生点来表示原点间的关系,构成的集合就有了丰富的含义。


例题5

题目

poj1733 Parity game

题解

先转换一下模型。
先来思考这样一个问题,如何快速表示出[l,r]中1的个数呢?
想到前缀和。设s[i]表示前i个位的和,则[1,i]的1的个数为s[i],进而可得[l,r]有s[r]-s[l-1]个1。
结合上题目问题,如果小A说[l,r]有奇数个1,那么有s[r]-s[l-1]为奇数,所以s[r]与s[l-1]的奇偶性不同。
同理,对于[l,r]的1个数为偶数,则有s[r]与s[l-1]的奇偶性相同。
所以,题意转化成:给出s[x]与s[y]的奇偶性是否相同,判断有无不合条件。
这样一来,题目就很像“程序自动分析”了,一个可传递的相同关系,和一个不可传递的、拿来检验的不同关系。
相同?简单水过!其实仍有区别。仔细思考,我们发现奇偶性不同也有传递性!如s[a]与s[b]的奇偶性不同,s[b]与s[c]的奇偶性不同,那么有s[a]与s[c]的奇偶性相同。

两种思路。先说说“扩展域”:
既然有这么一个两者循环关系,我们就给每个点拆成两个点,分别用a1[x]和a0[x]表示吧。并查集入门读者可以把1理解成奇数,0为偶数。
对于奇偶性相同,则merge( a1[x],a0[x] ),merge( a1[y],a0[y] );
对于奇偶性不同,则merge( a1[x],a0[y] ),merge( a1[y],a0[x] )。
两个点在一个集合中,说明它们奇偶性相同。
所以当说x与y的奇偶性相同,如果有fa[ a1[x] ] == fa[ a0[y] ],则说谎了。
当说x与y的奇偶性不同,如果有fa[ a1[x] ] == fa[ a1[y] ],也是说谎。

再讲讲“边带权”:
在一个集合中,因为是在给定条件下合并的,所以它们之间的关系一定是可以推出的。
既然关系都是已知的,不妨让每个点记录与集合代表的关系(0-相同,1-不同),这个关系可以用异或维护。
接下来就是考虑跨根状态的表示和合并两个并查集时d[fx]和d[fy]的关系。
1、在本问题中,跨根状态的表示很简单,对于同根的x,y,它们的关系是d[x]^d[y]。
2、合并要遵循上述关系。对于不同根的x,y,给定要求d[x]^d[y]=c(c=0或1)。接下来要考虑怎样的d[fx]能满足d[x]^d[fx]^f[y]=c,可得d[fx]=c^d[x]^f[y]。解决了这个问题,合并两个集合的问题也就解决了。

代码

扩展域代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=10010;

int n,m;
int tot=0,w[2*maxn];
int fa[4*maxn];

struct U{int x,y,ans;}a[maxn];

int find_fa(int x)
{
	if(fa[x]==x) return x;
	return fa[x]=find_fa(fa[x]);
}

void merge(int x,int y)
{
	int fx=find_fa(x),fy=find_fa(y);
	fa[fx]=fy;
}

char ch[10];
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&a[i].x,&a[i].y);a[i].x--;
		scanf("%s",ch);
		if(ch[0]=='e') a[i].ans=0;
		else a[i].ans=1;
		w[++tot]=a[i].x;w[++tot]=a[i].y;
	}
	sort(w+1,w+tot+1);
	n=unique(w+1,w+tot+1)-(w+1);
	
	for(int i=0;i<=2*n;i++) fa[i]=i;
	for(int i=1;i<=m;i++)
	{
		int ux=lower_bound(w+1,w+n+1,a[i].x)-w,uy=lower_bound(w+1,w+n+1,a[i].y)-w;
		if(a[i].ans==0)
		{
			if(find_fa(ux)==find_fa(uy+n)){printf("%d\n",i-1);exit(0);}
			merge(ux,uy);
			merge(ux+n,uy+n);
		}
		else
		{
			if(find_fa(ux+n)==find_fa(uy+n)){printf("%d\n",i-1);exit(0);}
			merge(ux+n,uy);
			merge(ux,uy+n);
		}
	}
	printf("%d\n",m);
	return 0;
}

边带权代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=1e5+10;

int n,m;
int tot=0,w[maxn*2];
struct U{int x,y,ans;}a[maxn];

int fa[maxn],d[maxn];
int find_fa(int x)
{
	if(x==fa[x]) return x;
	int root=find_fa(fa[x]);
	d[x]=d[x]^d[fa[x]];
	return fa[x]=root;
}

char ch[5];
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%s",&a[i].x,&a[i].y,ch);a[i].x--;
		a[i].ans=ch[0]=='o'?1:0;
		w[++tot]=a[i].x;
		w[++tot]=a[i].y;
	}
	sort(w+1,w+tot+1);
	n=unique(w+1,w+tot+1)-(w+1);
	
	for(int i=1;i<=n;i++)
	{
		fa[i]=i;
		d[i]=0;
	}
	for(int i=1;i<=m;i++)
	{
		int ux=lower_bound(w+1,w+n+1,a[i].x)-w;
		int uy=lower_bound(w+1,w+n+1,a[i].y)-w;
		int fx=find_fa(ux);
		int fy=find_fa(uy);
		if(fx==fy && (d[ux]^d[uy])!=a[i].ans){printf("%d\n",i-1);exit(0);}
		fa[fx]=fy;
		d[fx]=d[ux]^d[uy]^a[i].ans;
	}
	printf("%d\n",m);
	return 0;
}

 

例题6

题目

洛谷2024 [NOI2001]食物链

题解

这题较于例题4,每个点的状态都有3种。

对于“边带权”做法在已有介绍。
再简单讲讲我现在做这题的体会吧。
同样考虑两个问题:
1、虑跨根状态的表示:两个同根节点x,y,求x与y的关系。因为d[x]是root与x的关系,d[y]是root与y的关系,对d[x]和d[y]简单的加减乘除并不能连接x和y,因为d[x]和d[y]的关系都是自上向下传递的。我们考虑把y与root的关系求出来,这样又知道root与x的关系,把y->root和root->x连接起来可得y->root->x,所以x和y的关系就求出来了。至于如何把root->y变成y->root,只要3-d[y]就可以了。
2、集合的合并:咯咯咯~

“扩展域”同样可以解决这题。
一个点拆3个点,记为a1,a2,a3。有两个点x,y。对于相同操作分别合并x1-y1,x2-y2,x3-y3;对于x吃y操作,合并x1-y2,x2-y3,x3-y1。
为什么要拆成3个点呢?因为每3个吃的关系,即为同类。换言之,每3个为一个循环周期。
假话用文字表述:
x,y同类,若 x1与y2 或 y1与x2 同一集合为假话。
x吃y,若 x1与y1 或 y1与x2 同一集合为假话。

 

T4~6 Summarize

“边带权”“扩展域”的加盟使得并查集的阵容更加庞大,应用更加广泛。
“边带权”的关键是安排好合理的边权来表示元素间的关系,一定要考虑跨根状态的表示集合的合并两个问题。
“扩展域”以拆点为中心内容,如何连边也是一个需要考虑的问题。
总而言之,使用“边带权”也好,“扩展域”也好,目的都是处理好众多元素间的“传递关系”。在使用了“边带权”或“扩展域”之后,我们允许并查集处理不止一种的“传递关系”。
这就是并查集的进阶之处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值