Appearance
并查集是一片森林,一般的并查集通过路径压缩/按秩合并实现,将两者结合的均摊时间复杂度为
启发式合并
一般实现的并查集,通过维护每个节点的父节点实现快速锁定根节点。但是这并不能在任一时刻正确维护所有节点所在树的根节点,即:无法
考虑“真正意义上”的启发式合并,初始每个节点视作一个单独的集合,对每个节点维护一个根节点,每次合并两个节点
因为最终只会合并成一个集合,总均摊时间复杂度
常数相对较大。
采用链式前向星可适当减小常数。
点击展开代码
cpp
struct DSU
{
int root[N];
list<int> p[N];
void init(int n)
{
for (int i = 1; i <= n; i++)
root[i] = i, p[i].clear(), p[i].emplace_back(i);
}
bool same(int x, int y) { return root[x] == root[y]; }
void merge(int x, int y)
{
if (same(x, y))
return;
x = root[x], y = root[y];
if (p[x].size() > p[y].size())
{
swap(x, y);
}
for (auto u : p[x])
{
root[u] = y;
p[y].push_back(u);
}
p[x].clear();
}
} dsu;可撤销并查集
因为路径压缩不止在合并时,在判断连通性时,也会改变树的形态,并且改变树的的形态是时间复杂度的保证,所以路径压缩不可撤销。与之相比,按秩合并的合并操作是可撤销的。因为按秩合并只需要在合并时把一棵树的根接到另一棵树上即可,在找根节点时,暴力跳父节点。
因为撤销操作也是有顺序的,即按照合并的顺序逆序撤销,所以把合并操作放在栈中,每次撤销栈顶操作即可。合并撤销不影响子树结果,所以记录下是哪个节点接到哪个节点上即可(
点击展开代码
cpp
void init(int n) {
for (int i = 1; i <= n; i++)
fa[i] = i, sz[i] = 1;
while (history.size())
history.pop();
}
int find(int x) {
while (x != fa[x])
x = fa[x];
return x;
}
bool same(int u, int v) { return find(u) == find(v); }
void merge(int u, int v) {
if (same(u, v)) {
history.push(-1);
return;
}
u = find(u), v = find(v);
if (sz[u] < sz[v])
swap(u, v);
history.push(v);
sz[u] += sz[v];
fa[v] = u;
}
int History() { return history.size(); }
void roll() {
if (history.empty())
return;
auto t = history.top();
history.pop();
if (t == -1)
return;
sz[fa[t]] -= sz[t];
fa[t] = t;
}带权并查集
带权并查集维护节点到父节点的相对关系。以模
即 weight[x] 表示从儿子 weight 恒为
路径压缩前,若
约定 merge(x, y, w) 加入限制 find 后,记 weight[x] 和 weight[y] 已经分别表示
若按集合大小需要反向连接,把
当 weight[x] - weight[y] == w (mod 3);不满足说明新限制与已有信息矛盾。下面的 merge 以返回值表示限制是否一致,并使用按大小合并控制树高。
点击展开代码
cpp
int normalize(int value) {
value %= 3;
return value < 0 ? value + 3 : value;
}
void init(int n) {
for (int i = 1; i <= n; i++)
fa[i] = i, sz[i] = 1, weight[i] = 0;
}
int find(int x) {
if (fa[x] == x)
return x;
int parent = fa[x];
fa[x] = find(parent);
weight[x] = normalize(weight[x] + weight[parent]);
return fa[x];
}
bool same(int x, int y) {
return find(x) == find(y);
}
// 返回 false 表示新限制与已有关系矛盾。
bool merge(int x, int y, int w) {
int fx = find(x);
int fy = find(y);
w = normalize(w);
if (fx == fy)
return normalize(weight[x] - weight[y]) == w;
// 若连接 fx -> fy,这条边应具有的权值。
int fx_to_fy = normalize(w - weight[x] + weight[y]);
if (sz[fx] <= sz[fy]) {
fa[fx] = fy;
weight[fx] = fx_to_fy;
sz[fy] += sz[fx];
} else {
fa[fy] = fx;
weight[fy] = normalize(-fx_to_fy);
sz[fx] += sz[fy];
}
return true;
}在路径压缩和按大小合并下,
扩展域并查集
也有说法叫“种类并查集”的。在带权并查集中,通过点权表示节点的“种类”,通过边权的累计维护点权。在扩展域并查集中,通过对于每个点不同身份都维护一个“分身”节点来实现身份之间的判断。
同样以食物链为例:
判断
合并
具体实现和朴素并查集一致,是合并和判断的节点有区别。
可持久化并查集
支持历史版本的并查集。本质上就是把并查集的数组修改用单点修改、单点询问的可持久化线段树(可持久化数组)替代了。
这里不使用路径压缩:一次 find 可能修改路径上的多个父指针,持久化每个修改都要产生新的线段树节点,而且反复访问旧版本时不能沿用通常的均摊分析。改用按大小合并后,并查集树高为 find 只读取父指针,不改变历史版本。
设并查集有 find 最多经过 find、连通性查询或合并通常为
初始建树为
不过同样的问题,在支持离线的情况下,可以用可撤销并查集做到线性空间。具体而言,就是需要访问历史版本时,把当前版本编号指向历史版本编号,那么每个版本都只会指向一个比它小的编号,那么建出来的一定是一棵树,在这个树上递归执行合并操作,递归退出时撤销即可。
点击展开代码
cpp
p_tree fa, sz; // 使用可持久化化线段树维护
int n;
void init(int n) {
this->n = n;
vector<int> a(n + 1), b(n + 1);
for(int i = 1; i <= n; i++) a[i] = i, b[i] = 1;
fa.build(fa.root[0], 1, n, a);
sz.build(sz.root[0], 1, n, b);
}
int find(int k, int x) {
int fa;
while ((fa = this->fa.query(this->fa.root[k], 1, n, x)) != x){
x = fa;
}
return fa;
}
bool same(int k, int x, int y) { return find(k, x) == find(k, y); }
void merge(int p, int q, int x, int y) {
if (same(p, x, y)) {
copy(q, p);
return;
}
x = find(p, x), y = find(p, y);
int sz_x = sz.query(sz.root[p], 1, n, x),
sz_y = sz.query(sz.root[p], 1, n, y);
if (sz_x > sz_y) {
swap(x, y);
swap(sz_x, sz_y);
}
fa.update(fa.root[p], fa.root[q], 1, n, x, y);
sz.update(sz.root[p], sz.root[q], 1, n, y, sz_x + sz_y);
}
void copy(int now, int cur) {
fa.root[now] = fa.root[cur];
sz.root[now] = sz.root[cur];
}赋值并查集
维护把集合中所有
设预处理值域为
初始化并查集数组并扫描初始序列需要
点击展开代码
cpp
struct merge_dsu
{
int fa[N], sz[N], val[N], belong[N];
bool exist[N];
void init(int value_limit)
{
for (int i = 1; i <= value_limit; i++)
{
fa[i] = i;
sz[i] = 1;
val[i] = i;
}
}
void init(vector<int> &a)
{
for (auto u : a)
{
exist[u] = 1;
belong[u] = u;
}
}
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
int query(int initial_value)
{
return val[find(initial_value)];
}
int merge(int x, int y, int z)
{
x = find(x), y = find(y);
if (sz[x] > sz[y])
swap(x, y);
fa[x] = y;
sz[y] += sz[x];
val[y] = z;
return y;
}
void modify(int x, int y)
{
if (x == y)
return;
if (!belong[x])
return;
int rx = find(belong[x]);
if (!belong[y])
{
belong[y] = rx;
belong[x] = 0;
val[rx] = y;
}
else
{
int ry = find(belong[y]);
int root = merge(rx, ry, y);
belong[y] = root;
belong[x] = 0;
}
}
};倍增并查集
倍增并查集用于专门解决区间等价问题。
形如存在若干个限制
最后维护出所有等价类。
具体而言,令区间长度为
对每个
处理完
个合法区间节点,向下传递共执行
令
保留全部层需要
使用普通并查集维护即可。
