目录
例题1
题目
给一个无向图,求其中的连通块。
题解
并查集就是一个集合,并且以fa[x]为集合代表。
对于这题,如果x,y有连边,把它们放入一个集合中。
例题2
题目
题解
如果ai=aj则把i和j放入一个集合,
再处理完所有的相等关系后,把不等关系当作检验条件。
1<=x<=1e9当然要离散化。
T1~2 Summarize
并查集能在一张图中维护节点之间的连通性,擅长维护许多具有传递性的关系。所谓传递性,即A和B有关系,B和C有关系,那么A和C也有一定关系。
例题3
题目
题解
一个贪心策略是用一个二叉堆维护暂定所卖的商品,按过期顺序遍历,如果有新的利润更大的商品,则将其替换。另一个贪心策略:对于一个未选的利润最大商品,我们安排它在尽量靠近过期时间卖掉。
对于前一个贪心具体讲解及实现,这里不再是重点。我们谈谈第二种。
如何做到快速求到距离第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
题目
题解
在《并查集-应用》中已有详解,这里不再赘述。
在讲下一个例题之前,我要介绍一下《并查集-进阶》的重点知识。既然坚持看到了这里,不往下看半途而废就是失败!!!
并查集的两大操作“扩展域”和“边带权”,其中“边带权”在《并查集-应用》中已经有所介绍,那个所谓的“种类并查集”也可以归为“边带权并查集”。
先讲讲“边带权并查集”吧。并查集可以看作是一个森林,这就涉及到树论了。树中最常见的就是边权,点权,我们把那一套东西搬到并查集中来。
尽管两个节点同属于一个集合,如果做到这里,它们之间的关系并没有得到展现。为了表现一个集合中元素与元素间的关系,我们加入了边权,进而得到点权,不同的点权有不同的含义,这就使得一个集合内的元素的关系得以充分的展现。
“扩展域”类似于分层图,也就是拆点思想。把一个点拆分成多个点,每个点表示不同意义。在自此基础连边,就能用衍生点来表示原点间的关系,构成的集合就有了丰富的含义。
例题5
题目
题解
先转换一下模型。
先来思考这样一个问题,如何快速表示出[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
题目
题解
这题较于例题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
“边带权”和“扩展域”的加盟使得并查集的阵容更加庞大,应用更加广泛。
“边带权”的关键是安排好合理的边权来表示元素间的关系,一定要考虑跨根状态的表示和集合的合并两个问题。
“扩展域”以拆点为中心内容,如何连边也是一个需要考虑的问题。
总而言之,使用“边带权”也好,“扩展域”也好,目的都是处理好众多元素间的“传递关系”。在使用了“边带权”或“扩展域”之后,我们允许并查集处理不止一种的“传递关系”。
这就是并查集的进阶之处。