并查集及其扩展
文章首发于:My Blog 欢迎大佬们前来逛逛
基础并查集
并查集就是用来维护一些元素之间的关系的集合。
例如 A的亲戚是B ,则我们可以把 A与B放到同一个集合中,表示AB属于同一种关系(亲戚关系),并且标记A和B的关系数组都为B
B的亲戚是C,则我们可以把 B和C也放到同一个集合中,则BC也属于同一个集合,并且标记B和C的关系数组都为C
再往后我们需要知道A和C是否是 同一个关系(即是否具有传递性),我们可以通过一直查找A和C的关系数组直到没有关系为止,看看他们是否是相同的元素值,即可知道他们是否属于同一个关系:
- A的关系数组的值是 B,而B的关系数组的值是C,所以A的关系为C
- C的关系数组的值就是C,所以A和C属于同一关系
- 并查集的初始化
每个元素的关系数组的关系只有其一个人:A默认为A,B默认为B
void init(){ for (int i=1;i<n;i++){ parent[i]=i; } }
- 并查集的查找:(路径压缩)
每个元素在确定关系的时候都需要一直往上寻找,直到到达它的最顶级关系:A的关系不是B,而是往上B的关系,直到到达C,C的关系为其本身,这就到到达了A的最顶级关系,就是C
int find_set(){ if (x!=parent[x]){ parent[x]=find_set(parent[x]); } return parend[x]; }
- 并查集的合并
合并是并查集最重要的功能,就如同上述的A和B或者B和C的确认关系的时候,把他们合并到同一个关系中,A的关系是B,B的关系为其本身
void merge_set(int x,int y){ x=find_set(x); y=find_set(y); if (x!=y){ parent[x]=y; //X合并到Y上 } }
基础并查集便可以解决一系列的存在关系的问题
1. P1551 亲戚
这道题让我们判断两个人之间是否是亲戚,和我们上面的描述的基本是一致的,只需要判断他们两个是否属于同一个集合即可。
//TODO: Write code here int n,m,p; const int N=1e5+10; int parent[N]; void init_set(){ for (int i=1;i<=n;i++){ parent[i]=i; } } int find_set(int x){ if (x!=parent[x]){ parent[x]=find_set(parent[x]); } return parent[x]; } void merge_set(int x,int y){ x=find_set(x); y=find_set(y); if (x!=y){ parent[y]=x;//y合并到x上 } } signed main() { cin>>n>>m>>p; init_set(); for (int i=1;i<=m;i++){ int a,b; cin>>a>>b; merge_set(a,b); } while (p--){ int a,b; cin>>a>>b; if (find_set(a)==find_set(b)){ cout<<"Yes\n"; }else{ cout<<"No\n"; } } #define one 1 return 0; }
2. P1536 村村通
这道题其实就是求有多少个集合的问题。
如果一共有四个城市,有两条道路,分别为 1到3 和 3到4,则我们画图可以看出来 1 3 4 都属于同一个集合,2城市单独属于一个集合,总共有两个集合,因此如果要将两个集合合并,则需要添加的道路数量就是总集合数-1
//TODO: Write code here int n,m; const int N=1e5+10; int parent[N]; int find_set(int x){ if (x!=parent[x]){ parent[x]=find_set(parent[x]); } return parent[x]; } void merge_set(int x,int y){ x=find_set(x); y=find_set(y); if (x!=y){ parent[y]=x; //y合并到x } } void display(){ for (int i=1;i<=n;i++){ printf("i=%d,parent[%d]=%d\n",i,i,parent[i]); } cout<<endl; } signed main() { while (cin>>n>>m){ if (n==0){ break; } for (int i=1;i<=n;i++){ parent[i]=i; } while (m--){ int a,b; cin>>a>>b; merge_set(a,b); } int ans=0; for (int i=1;i<=n;i++){ //查找有几个不同的find_set if (find_set(i)==i) ans++;//统计总集合数 } cout<<ans-1<<endl; } #define one 1 return 0; }
种类并查集
种类并查集属于扩展域并查集一类。
不同的个体之间存在不同的关系,而每个相同个体之间都属于同一关系,不同的关系的个体的种类不同,因此就有了种类并查集。
1. P1892 [BOI2003]团伙
定义:
- 一个人的朋友的朋友是朋友
- 一个人的敌人的敌人是朋友
(很挺符合常理)
因此我们便可以确定两个并查集的种类:朋友和敌人。
不妨设每个人都有一个 enemies数组,表示他的敌人,则它的 enemies数组存储的可能是它的另一个敌人的朋友,即它的两个敌人互为朋友。
我们便可以确定出这样的关系:
- 如果A没有敌人:则A的敌人包括B和B的朋友们
- 如果A有敌人,则A的敌人是B的朋友
- 对于B同理
//a b是敌人 if (enemies[a]==0){ //如果a没有敌人,则a的敌人就包括b和b的朋友们 enemies[a]=find_set(b); } else{ //如果a有敌人,则a的敌人们就是b的朋友 merge_set(enemies[a],b); } if (enemies[b]==0){ //如果b没有敌人,则b的敌人就包括a和a的朋友们 enemies[b]=find_set(a); } else{ //如果b有敌人,则b的敌人们就是a的朋友 merge_set(enemies[b],a); }
也可以使用这种并查集的做法:
表达敌人与朋友两种关系,不妨将并查集的数组开大两倍,作为并查集的扩展域,于是可以表达朋友以及敌人两部分,朋友对应第一倍,敌人对应第二倍。于是我们可以得到如下合并公式
- A和B是敌人的情况:
- A的敌人:A+n 是B 的朋友
- B的敌人: B+n是A 的朋友
- A和B是朋友: A和B
if (ch=='E'){ //ab是敌人 则a+n与b,b+n与a是朋友 merge_set(a+n,b); merge_set(b+n,a); } else if (ch=='F'){ merge_set(a,b); }
2. P1525 [NOIP2010 提高组] 关押罪犯
如题所示,我们需要把多个罪犯分成不同的种类,而每对罪犯之间又存在着不同的仇恨值。
我们需要把这些罪犯分到两个种类区域中,使得合并后每两个罪犯之间的最大仇恨值为一个理论最小值。
我们显然需要对每对罪犯仇恨值按照从大到小的顺序排序。
如果A和B 是敌人,则A和B要尽量分在两个不同的区域中,那么A不能和B在一起,要和谁在一起呢?
我们可以让A和(B的其他敌人)在一起,因为按照贪心的原理来看,如果A和B有仇,则A和B的仇人不一定有仇,相反B的仇人可能和A是朋友,因为他们都和B有仇,当然这句话是我的补充,题中并没有说他们是朋友,只是为了便于理解。
因此寻找B仇人们,让他们和A是一伙的;同理寻找A的仇人们,让他们和B是一伙的。因为是按照仇恨值从大到小排序的,因此A和B的仇人们一伙,如果存在敌对关系的话,仇恨值一定是比B要小的(A和B仇恨值大,先判断,越往后肯定仇恨值越小);同理对于B和A的仇人们也是一样的。
一直把A和B的仇人们合并,B和A的仇人们合并。
在对A合并,寻找B的仇人的时候,直到B的仇人们的仇人的仇人.... 又回到了A,则A和B不可避免的又碰到了一起,即这个时候已经无法在分了,A和B不管找谁的仇人他们都是有仇的,则此时我们直接输出这个组合,此时这个组合关系的仇恨值一定是一个理论最小值,因为是按照从大到小排序的,既然不存在其他组合了,则这种一定是最小的。
//TODO: Write code here int n,m; const int N=2e5+10; int parent[N],enemies[N]; struct Node { int a,b,c; }node[N]; bool comp(Node a,Node b){ return a.c>b.c; } int find_set(int x){ if (x!=parent[x]){ parent[x]=find_set(parent[x]); } return parent[x]; } void merge_set(int x,int y){ x=find_set(x); y=find_set(y); if (x!=y){ parent[x]=y; } } bool check(int x,int y){ x=find_set(x); y=find_set(y); if (x==y){ return true; } return false; } void init(){ for (int i=1;i<=n;i++){ parent[i]=i; } } void display(){ for (int i=1;i<=n;i++){ printf("i=%d,parent[%d]=%d\n",i,i,parent[i]); } cout<<endl; } signed main() { cin>>n>>m; init(); for (int i=1;i<=m;i++){ cin>>node[i].a>>node[i].b>>node[i].c; } sort(node+1,node+1+m,comp); for (int i=1;i<=m+1;i++){ int a=node[i].a,b=node[i].b; if (check(a,b)){ cout<<node[i].c<<endl; break; } if (enemies[a]==0){ enemies[a]=b; } else{ merge_set(enemies[a],b); } if (enemies[b]==0){ enemies[b]=a; } else{ merge_set(enemies[b],a); } } #define one 1 return 0; }
3. P2024 [NOI2001] 食物链
[P2024 NOI2001] 食物链 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
我们在这个题中能得到三种关系:
- 属于同一类
- A吃B 的关系
- A被B吃 的关系
那么我们就可以创建三个种类,即三个集合,来分别合并他们之间的关系。
如果他们两个是同类的关系:输入1 u v
- u与v是同类(第一群系A)
- u+n和v+n是同类(第二群系B)
- u+n+n和v+n+n是同类(第三群系C)
merge_set(u,v) merge_set(u+n,v+n) merge_set(u+n+n,v+n+n)
如果它们两个是吃与被吃的关系: 输入 2 u v
由于他们三个群系属于一个环形生物链,并且同一群系中生物不能互吃
因此只能是 A群系吃B群系 , B群系吃C群系, C群系吃A群系
merge_set(u,v+n+n); // A的u被C的v吃 merge_set(u+n,v); // B的u被A的v吃 merge_set(u+n+n,v+n); // C的u被B的v吃
我们如何判断话的真假?
- 如果输入2 u v,则uv一定不属于同一群系,因为uv存在u吃v的关系。
如果它是个假的:
- 只需要判断uv属于同一群系,则False
- 或者判断 v吃u ,则不满足 u吃v,则False
则这两种情况如下:
分别表示 uv 是同一群系 或者 u被v吃
if (find_set(u)==find_set(v) || find_set(u)==find_set(v+n)) ans++;//假话++
- 如果输入 1 uv,则uv属于同一群系。
如果它是个假的:
- 只需要判断 存在 u吃v,则False;
- 或者判断 存在 v吃u,则False
则这两种情况如下:
if (find_set(v+n)==find_set(u) || find_set(u)==find_set(v+n)) ans++//假话
完整代码如下:
//TODO: Write code here int n,m; const int N=1e5+10; int parent[N*3]; void init(){ for (int i=1;i<=3*n;i++){ parent[i]=i; } } int find_set(int x){ if (x!=parent[x]){ parent[x]=find_set(parent[x]); } return parent[x]; } void merge_set(int x,int y){ x=find_set(x); y=find_set(y); if (x!=y){ parent[x]=y; } } signed main() { int ans=0; cin>>n>>m; init(); for (int i=1;i<=m;i++){ int opt,a,b; cin>>opt>>a>>b; if (a>n || b>n){ ans++; continue; } if (opt==1){ //判断是不是假话: a与b是同类 //如果a与b 存在 a吃b 或者 b吃a 的关系,则a和b是同类是假话 if (find_set(a)==find_set(b+n) || // a被b吃 find_set(b)==find_set(a+n)){ // b被a吃 ans++; } else{ //真话 merge_set(a,b); merge_set(a+n,b+n); merge_set(a+2*n,b+2*n); } } else if (opt==2){ //判断是不是假话: a吃b //如果a与b同类;或者b吃a 成立,则a吃b是假话 if (find_set(a)==find_set(b) || // a与b是同类 find_set(a)==find_set(b+n)){ // a被b吃 ans++; } else{ //真话 merge_set(a,b+n+n); // A的a被C的b吃 merge_set(a+n,b); // B的a被A的b吃 merge_set(a+n+n,b+n); // C的a被B的b吃 } } } cout<<ans; #define one 1 return 0; }
带权并查集
就是在维护集合关系的树中添加边权的并查集,这样做可以维护更多的信息。
不难发现所谓的并查集本质上也是一种树,由于路径压缩的存在,使得一个并查集树中,其被压缩过的子节点总是直接指向根节点,于是我们可以给根节点与子节点之间的边进行赋权。
这个权值可以表示该节点与根节点之间的距离
待更新。。。。
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17227827.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)