并查集: 1.将两个集合合并
2.询问两个元素是否在一个集合当中
基本原理:每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,f[x]
表示x的父节点
问题1:如何判断树根: if (f[x] == x)
问题2:如何求x的集合编号: while (f[x] != x) x = f[x];
问题3:如何合并两个集合: f[x]
是 x
的集合编号,f[y]
是 y
的集合编号。=> f[x]= y
时间复杂度分析
优化 | 平均时间复杂度 | 最坏时间复杂度 |
---|---|---|
无优化 | ||
路径压缩 | ||
按秩合并 | ||
路径压缩 + 按秩合并 |
直接认为反阿克曼函数α(n)的一个比logn还小的系数就行,一般算法竞赛中α(n)认为是一个小于等于5的系数。
(1)朴素并查集:
int f[N]; //存储每个点的祖宗节点
// ① 返回x的祖宗节点(无路径压缩)
int Find(int x){
if(f[x]==x) return x;
else return Find(f[x]);
}
// ② 返回x的祖宗节点(有路径压缩)
int find(int x)
{
if (f[x] != x) f[x] = find(f[x]);
return f[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) f[i] = i;
// 合并a和b所在的两个集合:!!!注意这里的合并 合并的不是改变当前节点a、b,而是将a、b所在集合的代表元素(根节点)进行合并
f[find(a)] = find(b);
(2)维护size的并查集:
int f[N], size[N];
//f[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (f[x] != x) f[x] = find(f[x]);
return f[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
f[i] = i;
size[i] = 1;
}
// 合并a和b所在的两个集合:先计算个数再对祖先赋值
size[find(b)] += size[find(a)];
f[find(a)] = find(b);
(3)维护到祖宗节点距离的并查集:
int f[N], d[N];
//f[]存储每个点的祖宗节点, d[x]存储x到f[x]的距离 即x到祖宗节点的距离
// 返回x的祖宗节点
int find(int x)
{
if (f[x] != x)
{
int u = find(f[x]);
d[x] += d[f[x]];
f[x] = u;
}
return f[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
f[i] = i;
d[i] = 0;
}
// 合并a和b所在的两个集合:
f[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量