冰茶姬(一种饮料?)
并查集(正经
并查集是一种支持动态 link 的,能够快速判断两元素是否处于同一集合的数据结构。
(如果需要 cut,请出门右转 LCT)
那么其基本操作就包括:
-
连边。在两个点之间连一条边,使得两个点所在的两个集合合并为一个集合。
-
查询。查询两个点是否处在同一个集合中。
这也是普通并查集的所有操作了。
暴力并查集
核心
当连边的时候,如果两个点本来就在一个集合里,是不需要再连边的。因此整个并查集的形态便是森林。
一般而言,树都是有根的,那么确定两个点分别属于哪个集合的时候,就可以找到并判断它们的根是否相等。
而连边的时候,也仅需要将两个根连接起来,因此找根是并查集的核心操作。
既然要找根,那么也就需要知道点与点之间的父(子)关系(确定父关系足矣)。
初始的时候,每个点各自为营,那么其父亲就是他们自己(我当我的爹)。
for(int i=1;i<=n;i++)fa[i]=i;
每次操作的时候,不管是连边还是查询,都需要先找到两个点所在树的根。
一个很朴素的想法就是,既然知道父关系,那么就根据父关系一路往上跳。
根据初始化,直到当前节点的父亲为自己,就是找到根了。
代码如下:
int find(int s) { if(s==fa[s])return s; return find(fa[s]); }
找到根之后,这两个操作也就很容易实现了:
for(int i=1;i<=m;i++) { op=re();u=re();v=re(); int r1=find(u),r2=find(v); if(op==1)fa[r1]=fa[r2]; else puts(r1==r2?"Y":"N"); }
时间复杂度
一般而言,时间复杂度都会有平均、最优、最差等几种。
最优一般是没有讨论意义的,因为总有好(du)人(liu)卡极限数据,那么此处就讨论一下极限数据时的最差情况。
在极限数据下,会出现这种情况:
每次将当前最大的树的根连在一个单节点树上,最终这棵树的深度则为 ,此时最坏情况下的单次查询的复杂度为 ,总复杂度为 。
这时候也就需要有一个优化。
路径压缩
由上可以发现,其实我们只期望知道根而不关心路径上通过了哪些点。
那么是否可以每次将我们所经过的点的父亲都更新成根呢?
当然可以,只需要将我们的 find
改成这样:
int find(int s) { if(s==fa[s])return fa[s]; return fa[s]=find(fa[s]); }
相当于是一次记忆化搜索,核心就在于 fa[s]=find(fa[s])
,在递归的过程中,不断更新经过的点的父亲,以达到路径压缩的目的,下次找 s
点的时候便能直接指向其根。
时间复杂度
这个优化计算时间复杂度是通过势能分析得到的均摊复杂度,证明在这(反正我是看不懂)。
按秩合并 / 启发式合并
除了路径压缩,还有一个很简单的想法,每次将小的合并到大的身上,这样就可以避免出现深度过深的情况了。
按秩合并和启发式合并是两种计算大小的方法,一种是计算树的大小,另一种是计算树的深度。
由于二者十分相似,一般不会太过细致的区分,不同的文章写的也不一样。
按大小(秩)的话比较简单,直接比较两个树的 siz
大小,将小的合并到大的上即可。
int find(int x){return fa[x]^x?(find(fa[x])):x;} void merge(int x,int y) { x=find(x),y=find(y); if(siz[x]>siz[y])swap(x,y); fa[x]=y;siz[y]+=siz[x]; }
但是实测按大小合并的复杂度并不优秀。
而按深度合并(启发式)则稍微复杂一些,因为如果两个树的深度不相等的话,小树合并到大树是不会影响大树的深度的,而两树深度相同的时候,合并才会使整体的深度 +1。
int find(int x){return (fa[x]^x)?(find(fa[x])):x;} void merge(int x,int y) { x=find(x),y=find(y); if(dep[x]>dep[y]) swap(x,y); fa[x]=y; if(dep[x]==dep[y]) dep[y]++; }
这两种方法的 find
则不需要使用路径压缩。
当然,这两种优化方式是可以一起使用的,就是启发式合并的时候也使用路径压缩。但是对于大多数数据而言,单独使用两种优化也是足够优秀的,所以同时使用两种优化并不比单独使用其中一种优秀很多。
完整代码自己打(懒
一道好题。
需要用到离散化。
基础应用
并查集的应用有最小生成树,虽然一些其他算法也有用到并查集(比如某些 DP,左偏树),但是 kruskal 可以说是其代表了。
边带权
思路:
显然这是一道并查集判连通的题目,但是不同之处在于,这道题不仅需要判断两个点是否处于同一连通块,查询时需要知道两个点之间的距离。
如果用一个二维数组去储存两点之间的距离的话,需要的空间是 的,显然不允许。
思考并查集有一个什么特点。没错,每个点都能很容易的找到自己的根节点。那么如果我们储存每个点到其根节点的距离,如果两个点在同一个集合里时,就可以通过到根节点的距离之差来代表两个点之间的距离,空间复杂度 ,可以接受。
这种需要考虑点之间距离的并查集,就叫做边带权。
用 dis
来代表当前节点到根节点的距离,siz
代表当前并查集的大小。
找根时,除了需要更新 fa
,还需要更新 dis
:
int find(int x) { if(fa[x]==x)return x; int f=find(fa[x]); dis[x]+=dis[fa[x]]; return fa[x]=f; }
简单分析可以发现,多次 find
的时候,每个节点的 dis
并不会多次更新。
合并的时候,同样需要更新 dis
,不过只需要更新根节点。同时更新根节点的 siz
:
void merge(int x,int y) { int r1=find(x),r2=find(y); fa[r2]=r1; dis[r2]=siz[r1]; siz[r1]+=siz[r2]; }
查询操作则最为简单。
完整代码:
const int inf=3e4+7; int n,fa[inf]; int siz[inf],dis[inf]; int find(int x) { if(fa[x]==x)return x; int f=find(fa[x]); dis[x]+=dis[fa[x]]; return fa[x]=f; } void merge(int x,int y) { int r1=find(x),r2=find(y); fa[r2]=r1; dis[r2]=siz[r1]; siz[r1]+=siz[r2]; } int main() { n=re(); for(int i=1;i<=inf;i++) fa[i]=i,siz[i]=1; for(int i=1;i<=n;i++) { char op[10]="";scanf("%s",op); int x=re(),y=re(); if(op[0]=='M')merge(x,y); else { int r1=find(x),r2=find(y); if(r1^r2)puts("-1"); else wr(abs(dis[x]-dis[y])-1),putchar('\n'); } } return 0; }
拓展域
拓展域一般用于解决一个点有多种性质的问题,如上。
一个人可以分为两个域:朋友域和敌人域。
根据朋友的朋友是朋友,可以得到朋友域处于同一个集合代表这些人互为是朋友。
而根据敌人的敌人是朋友,如果小甲和小乙互为敌人,小乙和小丙互为敌人,那么小甲和小丙应该是朋友,但是小甲和小丙不能直接通过朋友域连接,而是应该通过小乙的敌人域进行连接。
所以敌人域不会直接相连,它仅仅起到对朋友域进行间接连接的作用。
理论知识比较简单,而代码实现时,一般用 i,i+n
等来代表第 个人的不同域。
完整代码:
const int inf=2e3+7; int n,m,ans,fa[inf]; bool vis[inf]; int find(int x){return (fa[x]^x)?(fa[x]=find(fa[x])):(x);} int main() { n=re();m=re(); for(int i=1;i<=n*2;i++)fa[i]=i; for(int i=1;i<=m;i++) { char op[2]="";scanf("%s",op); int u=re(),v=re(); if(op[0]=='F') fa[find(u)]=find(v); if(op[0]=='E') { fa[find(u)]=find(v+n); fa[find(v)]=find(u+n); } } for(int i=1;i<=n;i++) { int ls=find(i); if(vis[ls])continue; vis[ls]=1;ans++; } wr(ans); return 0; }
(这个图可以点的啊喂)
可持久化
其实仅仅是用可持久化数组优化了并查集的 fa
和 dep
数组,使得并查集变得可持久化。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现