并查集是一种树形数据结构,经常用于处理一些集合之间的操作,例如元素查找、集合合并等等。
不同集合在并查集中以不同的树来表示,一般每棵树的根节点会作为当前集合的代表元。
查询两个元素是不是同一个集合里,只需要比较两个元素所在集合的代表元素。
并查集的初始化
假设一开始有n个元素,这些元素初始都是独立的。显然他们构成了n个集合,每个集合的代表元就是这些元素自己。
const int maxn =100010;
int fa[maxn + 1];//fa数组记录每个元素由谁代替
int sz[maxn + 1];//sz数组记录每个集合的元素个数
int dep[maxn + 1];//dep数组记录每个集合的树深度
void initial(int n){
for(int i = 1;i <= n; i++){
fa[i] = i;
sz[i] = dep[i] = 1;
}
}
集合合并
如果我们要将两个元素x, y所在的集合合并。先找到x, y 对应的代表元(fa等于自己的元素)。将其中一个代表元的fa指向另外一个。
int findset(x){
if(fa[x] == x)
return x;
return findset(fa[x]);
}
void Union(int x, int y){
int fx = findset(x), fy = findset(y);
if(fx == fy) return;
fa[fx] = fy;
}
路径压缩
我们可以缩短并查集中的路径,具体做法就是在查询的过程中,把沿途的每个节点的fa都设为集合代表元。
int findset(int x){
if(fa[x] == x)
return x;
fa[x] = findset(fa[x]);
return fa[x];
}
//可以简写为
int findset(int x){
return x == fa[x] ? x : (fa[x] = findset(fa[x]));
}
*启发式合并
在合并集合的时候,我们尽量选择包含元素个数少的集合,将他合并到另一个集合中,使需要改变代表元的元素数量尽可能少。将较小的集合合并到较大的集合中称为启发式合并。
void Union(int x, int y){
int fx = findset(x), fy = findset(y);
if(fx == fy) return;
if(sz[fx] > sz[fy])
swap(fx, fy);
fa[fx] = fy;
sz[fy] += sz[fx];
}
*按深度合并
在每次合并的时候将深度较小的集合并到深度较大的一方,并更新一下新集合的深度。
在路径压缩的时候,有可能会破坏我们维护的深度值,但算法总体复杂度不会变差。
void Union(int x, int y){
int fx = findset(x), fy = findset(y);
if(fx == fy) return;
if(def[fx] > def[fy])
swap(fx, fy);
fa[fx] = fy;
if(def[fx] == def[fy])//只有两颗树深度相等时才会更新
dep[fy]++;
}
*时间复杂度
易证使用启发式合并,当有n个元素和m次查询时,并查集的时间复杂度为O(m log n)。
一般我们认为并查集的时间复杂度为O(mα(m,n))。其中α是阿克曼函数的反函数,一个很小的常数
例题
1.修路
有 n 个城市,城市的编号为 1 到 n。城市之间已经修好了 m 条道路,第 i 条道路连接了第 xi 和第 yi个城市。请问最少再修几条道路,可以使得所有城市都连通(也就是说,我们可以通过道路从任意一个城市到另一个城市)?
输入格式
第一行两个整数 n,m,代表城市数量和已修道路数。接下来 m行,每行两个整数 x,y,表示一条连接第 x 个和第 y个城市的道路。
输出格式
输出一行一个数表示答案。
数据规模
对于所有数据,保证 1≤n,m≤100000,1≤x,y≤n,x≠y。
#include<bits/stdc++.h>
using namespace std;
int n, m, fa[100001];
int findset(int x){
if(x == fa[x])
return x;
fa[x] = findset(fa[x]);
return fa[x];
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
fa[i] = i;
for(int i = 1; i <= m; i++){
int x, y;
scanf("%d%d", &x, &y);
int fx = findset(x), fy = findset(y);
if(fx == fy)
continue;
fa[fx] = fy;
}
int cnt = 0;
for(int i = 1; i<= n; i++)
if(fa[i] == i)
++cnt;
printf("%d", cnt - 1);
}
2.行进路线
小蜗在玩一个游戏,游戏里有一张平面地图,他需要从坐标 (0,0) 点移动到坐标 (xe,ye)点。基于游戏设定,整张地图都十分危险,小蜗不能踏入其中。幸运的是,地图中存在 n个安全区域,第 i 个安全区域是由圆心在坐标 (xi,yi) 点,效果半径为 ri 的圆形信标展开形成的,安全区域可以相互重叠。特别的,除了上述 n 个安全区域,还存在一个圆心坐标为 (0,0) 点,效果半径为 1的安全区域。小蜗只能在安全区域中移动,如果两个安全区域相交或相切,小蜗可以从其中一个安全区域走到另一个安全区域。请问小蜗能不能顺利到达终点?能的话输出 1
,不能输出 0
。
输入格式
输入包含多组测试数据。第一行一个整数 T,表示数据组数。
对于每组数据,第一行两个整数 xe,ye,表示终点坐标。
接下来一行一个整数 n,表示安全区域的总数(不包括特别的那个安全区域)。
接下来 n行,每行三个整数 xi,yi,ri,表示一个安全区域。
输出格式
对于每组数据,输出一行一个数表示答案。
数据规模
对于所有数据,保证 1≤T≤10,1≤n≤1000,−108≤xi,yi,xe,ye≤108,1≤ri≤108。
#include<bits/stdc++.h>
using namespace std;
int n, cnt, ans, dist[100001], f[100001], pre[100001];
vector<int>edges[100001];
inline void dfs(int x){
for(auto y : edges[x])
if(y != pre[x]){
pre[y] =x;
dist[y] = dist[x] + 1;
dfs(y);
}
}
inline void solve(int x){
++cnt;
for(auto y : edges[x])
if(y != pre[x]){
pre[y] = x;
solve(y);
}
}
int main(){
scanf("%d", &n);
for(int i = 1; i <=n; i++){
int x, y;
scanf("%d%d", &x, &y);
edges[x].push_back(y);
edges[y].push_back(x);
}
for(int i = 1; i <= n; i++){
f[i] = 0;
memset(pre, 0, sizeof(pre));
for(auto y : edges[i]){
cnt = 0;
pre[y] = i;
solve(y);
f[i] = max(f[i], cnt);
}
}
int idx = 0, v = 1 << 30;
for(int i = 1; i <= n; i++)
if(f[i] < v){
v = f[i];
idx = i;
}
memset(dist, 0, sizeof(dist));
memset(pre, 0, sizeof(pre));
pre[idx] = -1;
dfs(idx);
ans = 0;
for(int i = 1; i <= n; i++)
ans += dist[i];
printf("%d\n", ans);
}
1LL的使用