由如下几部分构成:
集合 : parent , size(代表深度或融合大小,大的融合小的)变量 :n(并查集节点总数) , setCount(当前连通变量总数)方法 :
- 构造函数初始化(iota)
- find
- merge
并查集算法步骤:
- 确定并查集数据是什么?
- 例子:比如说从A点到达B点,说明应存取position。当connected(A,B)时,说明存在路径到达AB
- 确定按什么顺序合并?
- 最短路径(最大值-最小值)例子
- 将所有路径按从小到大的顺序排列
- 根据路径权值顺序,将位置加入并查集,知道满足connect(A,B)条件
- 下雨游泳例子
- 将当前高度按从小到大顺序排列
- 根据高度从小到大遍历,将位置加入并查集,直到connect(A,B)满足
并查集基础版本
初始化
假如有编号为1, 2, 3, …, n的n个元素,我们用一个数组fa[]来存储每个元素的父节点(因为每个元素有且只有一个父节点,所以这是可行的)。一开始,我们先将它们的父节点设为自己。
int fa[MAXN];inline void init(int n){for (int i = 1; i <= n; ++i)fa[i] = i;}
查询(根节点/代表元素)
根节点的标志就是父节点是本身
int find(int x){if(fa[x]==x){return x;}else{return find(fa[x]);}}
合并
先找到两个集合的代表元素(根节点),然后将前者的父节点设为后者即可
inline void merge(int i,int j){fa[find(i)]=find(j);}
优化方案
路径压缩(查询过程中进行)
在查询的过程中,把沿途的每个节点的父节点都设为根节点即可。下一次再查询时,我们就可以省很多事。
int find(x){if(fa[x]==x){return x;}else{fa[x]=find(fa[x]); //将父节点设置为根节点return fa[x];}/* 一行 */return fa[x]==x ? x : (fa[x]=find(fa[x]));}
按秩(不同合集的深度)合并
秩小的往秩大的合并
int fa[MAXN];int rank[MAXN];inline void init(int n){for(int i=1;i<=n;i++){fa[i]=i;rank[i]=1;}}inline void merge(i,j){int x=find(i),y=find(j);if(rank[x]<=rank[y]){fa[x]=y;}else{fa[y]=x;}if(rank(x)==rank(y) && x!=y){rank[y]++; /*深度相同且根节点不同,合并的树深度加一*/}}
C++11模板
class UnionFind {public:vector<int> parent;vector<int> size;int n;/* 当前【连通分量】数 */int setCount;public:UnionFind(int _n): n(_n), setCount(_n), parent(_n), size(_n, 1) {iota(parent.begin(), parent.end(), 0);/*itoa 自增+1*/}int findset(int x) {return parent[x] == x ? x : (parent[x] = findset(parent[x]));}bool unite(int x, int y) {x = findset(x);y = findset(y);if (x == y) {return false;}if (size[x] < size[y]) {swap(x, y);}parent[y] = x; /*将小秩合并到大秩序*/size[x] += size[y];--setCount; /*合并后【连通分量数】-1*/return true;}bool connected(int x, int y) {x = findset(x);y = findset(y);return x == y;}};/* 合理范围内也需要对基础款进行魔改 ———— 婴儿名字 */class UnionFind{public:unordered_map<string,string> parent_;unordered_map<string,int> size_;int n_; /* 当前连通变量数 */public:UnionFind(int _n):n_(_n){}string find(string x){if(parent_.find(x)==parent_.end()){++n_;parent_[x]=x;return x;}return x==parent_[x] ? x : (parent_[x]=find(parent_[x]));}void merge(string i,string j){string x=find(i),y=find(j);if(x==y){return;}if(x<y){swap(x,y);}/*字典序最小的为根元素*/parent_[x]=y;size_[y]+=size_[x];/* 被合并的规模至0,便于输出答案 */size_[x]=0;n_--;}bool connected(string i,string j){return find(i)==find(j);}int getSize(string x){return size_[x];}};
例题详解
亲戚关系(基础)
题目背景 若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。 题目描述 规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。 输入格式 第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。 以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有亲戚关系。 接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。 输出格式
P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。
const int MAXN=5005;int fa[MAXN];int rank[MAXN];inline void init(){for(int i=0;i<n;i++){fa[i]=i;rank[i]=1;}}int find(x){return fa[x]==x ? x : (fa[x]=find(fa[x]));}int merge(i,j){int x=find(i),y=find(j);if(rank[x]<=rank[y]){fa(x)=y;}else{fa(y)=x;}if(rank[x]==rank[y] && x!=y){rank[y]++;}}int main(){int n,m,p;cin>>n>>m>>p;init(n);for(int i=0,x,y;i<m;i++){cin>>x>>y;merge(x,y);}for(int i=0,x,y;i<p;i++){cin>>x>>y;printf("%s\n",find(x)==find(y)? "YES" : "NO");}return 0;}
奶酪(提高)
题目描述
现有一块大奶酪,它的高度为
,它的长度和宽度我们可以认为是无限大的,奶酪中间有许多半径相同的球形空洞。我们可以在这块奶酪中建立空间坐标系,在坐标系中, 奶酪的下表面为
,奶酪的上表面为
。
现在,奶酪的下表面有一只小老鼠 Jerry,它知道奶酪中所有空洞的球心所在的坐标。如果两个空洞相切或是相交,则 Jerry 可以从其中一个空洞跑到另一个空洞,特别地,如果一个空洞与下表面相切或是相交,Jerry 则可以从奶酪下表面跑进空洞;如果一个空洞与上表面相切或是相交,Jerry 则可以从空洞跑到奶酪上表面。
位于奶酪下表面的 Jerry 想知道,在 不破坏奶酪 的情况下,能否利用已有的空洞跑到奶酪的上表面去?
空间内两点
、
的距离公式如下:
输入格式
每个输入文件包含多组数据。
的第一行,包含一个正整数
,代表该输入文件中所含的数据组数。
接下来是
组数据,每组数据的格式如下: 第一行包含三个正整数
和
,两个数之间以一个空格分开,分别代表奶酪中空 洞的数量,奶酪的高度和空洞的半径。
接下来的
行,每行包含三个整数
,两个数之间以一个空格分开,表示空 洞球心坐标为
。
输出格式
行,分别对应
组数据的答案,如果在第
组数据中,Jerry 能从下表面跑到上表面,则输出
Yes,如果不能,则输出No(均不包含引号)。

可以将所有空洞划分若干个集合,一旦两个空洞相切或者相交,则将其放入同一个集合中。
还可以划分两个特殊集合,分别为顶部和底部,显然其执行逻辑和空洞相同。若相切或者相交,则放入同一集合。
题目可转化为判断顶部和底部是否在同一集合中。
顶部和底部相较,则说明有一个通路存在。
const int MAXN=1005;int fa[MAXN],rank[MAXN];using POS=int[3];vector<POS> _pos;/*init find merge*/bool isConnected(int[] _p1,int[] _p2,int r){return (pow(_p1[0]-_p2[0],2)+pow(_p1[1]-_p2[1],2)+pow(_p1[2]-_p2[2],2))<=pow(2*r,2);}int main(){int T;cin>>T;for(int i=0;i<T;i++){int n,h,r;cin>>n>>h>>r;init(n);fa[1001]=1001;fa[1002]=1002;for(int i=0;i<n;i++){cin>>_pos[i][0]>>_pos[i][1]>>_pos[i][2];if(_pos[i][2]<=r)merge(i,1001);if(h-_pos[i][2]<=r);merge(i,1002);}for(int i=0;i<n;i++)for(int j=i+1;j<n;j++){if(isConnected(_pos[i],_pos[j])){merge(i,j);}}printf("%s\n",find(1001)==find(1002) ? "yes":"no");}}
最小体力消耗路径
你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ,其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格子 (0, 0) ,且你希望去最右下角的格子 (rows-1, columns-1) (注意下标从 0 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。
一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。
请你返回从左上角走到右下角的最小 体力消耗值 。
思路:
将mn个节点放入并查集中,并实时维护其连通性。当某个集合中同时存在
0和m*n-1(其代表节点相同)时,便说明此路径是合法路径。
由于目标是寻找体力最小的一条路径,故将所有路径从小到大进行排列,当合并的新路径权值为v时,且满足上述条件时,则说明v为该路径的最小体力值,返回该值即可。面试题 17.07. 婴儿名字
每年,政府都会公布一万个最常见的婴儿名字和它们出现的频率,也就是同名婴儿的数量。有些名字有多种拼法,例如,John 和 Jon 本质上是相同的名字,但被当成了两个名字公布出来。给定两个列表,一个是名字及对应的频率,另一个是本质相同的名字对。设计一个算法打印出每个真实名字的实际频率。注意,如果 John 和 Jon 是相同的,并且 Jon 和 Johnny 相同,则 John 与 Johnny 也相同,即它们有传递和对称性。
在结果列表中,选择 字典序最小 的名字作为真实名字。
class UnionFind{public:unordered_map<string,string> parent_;unordered_map<string,int> size_;int n_;public:UnionFind(int _n):n_(_n){}string find(string x){if(parent_.find(x)==parent_.end()){++n_;parent_[x]=x;return x;}return x==parent_[x] ? x : (parent_[x]=find(parent_[x]));}void merge(string i,string j){string x=find(i),y=find(j);if(x==y){return;}if(x<y){swap(x,y);}/*字典序最小的为根元素*/parent_[x]=y;size_[y]+=size_[x];size_[x]=0;n_--;}bool connected(string i,string j){return find(i)==find(j);}int getSize(string x){return size_[x];}};class Solution {public:vector<string> trulyMostPopular(vector<string>& names, vector<string>& synonyms) {int N=names.size();UnionFind unionFind_(N);for(const auto &str:names){int i=str.find('(');int j=str.find(')');string name=str.substr(0,i);unionFind_.parent_[name]=name;int cnt=atoi(str.substr(i+1,j-i-1).c_str());unionFind_.size_[name]=cnt;}for(const auto &str:synonyms){int i=str.find(',');string left=str.substr(1,i-1);string right=str.substr(i+1,str.size()-i-2);unionFind_.merge(left,right);}vector<string> ans;int i=0;for(const auto& v:unionFind_.size_){if(v.second!=0 && i!=unionFind_.n_){string tmp=v.first+'('+to_string(v.second)+')';ans.emplace_back(tmp);i++;}}return ans;}};
