洛谷P5247-动态图完全连通性-LCT解法
jerry3128 和 zghtyarecrenj 大佬都讲得很好,但是漏了一些细节。这里简单补充一点。
Holm-de Lichtenberg-Thorup 算法的原始论文:https://u.cs.biu.ac.il/~rodittl/p723-holm.pdf
算法的简化描述如下。
给边分层,每条边一开始都在第 0 层,然后之后可以移到上层,定义 \(l(e)\) 为边 \(e\) 的层号。定义 \(G_i\) 为所有层号小于等于 \(i\) 的边构成的图。令\(F_0\) 为 \(G_0\) 的生成树森林,令 \(F_i\) 为 \(G_i \cap F_0\)。令 \(T_i\)为 \(F_i\) 中层号刚好为 \(i\) 的边的集合,即层号刚好为 \(i\) 的树边的集合。令 \(N_i\) 为 \(G_i-F_i\) 中层号刚好为 \(i\) 的边的集合,即层号为 \(i\) 的非树边的集合。下面我们只维护 \(F_i\)、\(T_i\)、\(N_i\)。
我们维护这几个性质:
-
对任何非树边 \(e=(x, y)\),\(x\) 和 \(y\) 都在 \(F_{l(e)}\)中连通。
-
\(F_i\) 中每个连通分量的节点数最多为 \(\lfloor n/2^i \rfloor\),这样最大的层号 \(l_{max}\)就是 \(\lfloor log_2(n) \rfloor\)。
-
对任何树边 \(e=(x, y)\),对任何 \(i>l(e)\),\(x\) 和 \(y\) 在 \(F_i\)中都不连通。
- 查询 \(x\) 和 \(y\) 是否连通
直接查询 \(x\) 和 \(y\) 在 \(F_0\) 中是否连通即可。
- 插入边 \(e = (x, y)\)
令 \(l(e)=0\),然后判断 \(x\) 和 \(y\) 是不是连通的,如果连通说明它是非树边,将其加入到 \(N_0\) 中。如果不连通说明它是树边,加入到 \(F_0\) 和 \(T_0\) 中。
- 删除边 \(e = (x, y)\)
先看 \(x\) 和 \(y\) 在不在 \(N_{l(e)}\) 中,如果在就可以直接从 \(N_{l(e)}\) 中删掉它,不影响连通性。
如果不在 \(N_{l(e)}\) 中,那肯定在 \(T_{l(e)}\)中。将其从 \(T_{l(e)}\),以及 \(F_0, F_1, \cdots, F_{l(e)}\) 中删去。然后我们就要尝试找一条边将 \(x\) 和 \(y\) 所在的连通分量重新连接起来,由于性质 1 和性质 3,这样的边的层号必须小于等于 \(l(e)\)。我们从 \(l(e)\) 层开始,一直尝试到第 0 层,如果在某层找到了,就成功,否则就失败。
- Reconnect(\((x, y)\), \(l\)),在第 \(l\) 层尝试重新连接 \(x\) 和 \(y\) 所在的连通分量
假设 \(X\) 为 \(x\) 所处的连通分量,\(Y\) 为 \(y\) 所处的连通分量。不失一般性,假设 \(X\) 的节点数不多于 \(Y\) 的节点数。我们搜索所有在 \(X\) 中的非树边 \(e = (u, v)\),如果 \(v\) 在 \(Y\) 中的话,就说明 \(e\) 可以连接两个连通分量,将 \(e\) 从非树边变成树边,即将 \(e\) 从 \(N_l\) 中删除,并把 \(e\) 加入到 \(T_l\) 以及 \(F_0, F_1, \cdots, F_l\) 中。
如果 \(v\) 不在 \(Y\) 中,那肯定在 \(X\) 中。我们将 \(e\) 移动到上层去,即令 \(l(e)\) 自增 1。因为我们要维护性质 1,所以我们要保证 \(u\) 和 \(v\) 在 \(F_{l+1}\) 中连通。我们发现,只要我们让 \(X\) 的生成树的所有边都在 \(F_{l+1}\) 层中就可以了。所以在开始搜索 \(X\) 的非树边前,我们先将 \(T_l\) 中在 \(X\) 中的边全部移动到 \(T_{l+1}\) 中,并且在 \(F_{l+1}\)中也加入这些边。
我们假设性质 2 在第 \(i\) 层成立,即第 \(i\) 层中所有连通分量的节点数为 \(\lfloor n/2^i \rfloor\)。第 \(i+1\) 层中的连通分量只在将第 \(i\) 层的树边移动到第 \(i+1\) 层中时改变,所以我们只要证明在移动过程中变化的连通分量的节点数最多为 \(\lfloor n/2^{i+1} \rfloor\) 即可。将第 \(i\) 层的树边移动到第 \(i+1\) 层时,相当于在第 \(i+1\) 层将一些小连通分量合并成一个点集等于 \(X\) 中的点集的连通分量,而由于 \(X\) 的节点数不超过 \(\lfloor n/2^{i+1} \rfloor\),所以第 \(i+1\) 层中所有的连通分量的节点数都不超过 \(\lfloor n/2^{i+1} \rfloor\)。所以性质 2 成立。
由于最大层数为 \(O(\log n)\),而且边的层号单调递增,层号不变时,边只会从非树边变成树边。所以每条边我们都只会操作 \(O(\log n)\) 次。假如我们可以将每次操作的时间控制在 \(O(\log n)\),那么我们就得到了一个插入和删除的均摊时间为 \(O(\log^2 n)\) 的算法。
从上面的描述中我们可以看到,对一条边时只有以下几种操作:
-
查询某点在 \(F_i\) 里的哪棵树中。
-
用这条边将两棵树连起来,或者将这条边删掉,得到两棵树。
-
将这条边加入到 \(N_i\) 中,或者从 \(N_i\) 中删掉。
-
将这条边加入到 \(T_i\) 中,或者从 \(T_i\) 中删掉。
-
遍历以 \(F_i\) 中指定子树里的节点为端点的 \(N_i\) 和 \(T_i\) 里的所有边。
其中,查询在哪棵树中,以及在森林里增删树边可以通过用 LCT 或者 ETT 来维护 \(F_i\) 实现 \(O(\log n)\) 的均摊复杂度。
遍历与指定子树相连的 \(N_i\) 和 \(T_i\) 中的边时,显然不可以直接遍历这个子树的所有节点,这样每次找到一条边的最坏情况复杂度是 \(O(n)\) 的。注意到,我们只需要做到在 \(O(\log n)\) 的时间内找到一条边即可。假如我们要遍历与指定子树相连的 \(N_i\) 里的边,并且我们用 LCT 来维护 \(F_i\),那我们可以在 LCT 中维护 auxiliary tree 的每个子树内的任意一个存在某条 \(N_i\) 中的边与之相连的点的编号,将这个编号称为 tag。要找一条与原图中的指定子树相连的 \(N_i\) 中的边时,只需要从 auxiliary tree 的根节点中拿出 tag,假设是 \(u\),然后找以 \(u\) 为端点的 \(N_i\) 中的点即可。
LCT 的节点只有指向 preferred child 的指针,没有指向 non-preferred children 的指针,但是维护子树信息需要从 non-preferred children 所在的子树的信息。通常这是通过 Top Tree 来实现的,即用 splay 来合并 non-preferred children 所在的子树的信息,然后通过根将合并后的信息传给节点(参考:https://www.cnblogs.com/Khada-Jhin/p/9743397.html)。但是这样代码量太大了。
注意到,我们其实不需要将所有 non-preferred children 所在的子树的信息合并起来,我们只需要从有 tag 的子树中随便选一个,就可以把它的 tag 作为当前节点的 tag 。所以我们只需要在每个节点用一个哈希表来维护所有有 tag 的 non-preferred children 的编号,更新节点 \(x\) 的 tag 的流程如下。
if (有边连着自己) {
x.tag = x
} else if (x.left_child.tag != NIL) {
x.tag = x.left_child.tag
} else if (x.right_child != NIL) {
x.tag = x.right_child.tag
} else if (x.tagged_non_preferred_children is not empty) {
c = x.tagged_non_preferred_children里的任意一个元素
x.tag = c.tag
} else {
x.tag = NIL
}
向 \(N_i\) 中插入边时,假如某个点 \(u\) 原先没有与之相连的 \(N_i\) 中的边,那就 access(\(u\)),再将这条边插入到 \(N_i\) 中,从而保证 tag 都是有效的。同样,删除时,如果某个点 \(u\) 原先只有一条与之相连的 \(N_i\) 中的边,那就 access(\(u\)),再将这条边从 \(N_i\) 中删掉。
动态图完全连通性截至2017年的最新成果汇总:https://www.cs.cmu.edu/~anupamg/advalgos17/handouts/italiano-talk-extract.pdf
其中这篇文章做到了 \(O(\log n / \log \log \log n)\) 的连通性查询以及均摊复杂度为 \(O(\log n (\log \log n)^3)\) 的更新:
Thorup M. Near-optimal fully-dynamic graph connectivity[C]//Proceedings of the thirty-second annual ACM symposium on Theory of computing. 2000: 343-350.
给我的个人博客打个广告:https://seekstar.github.io/2021/11/20/洛谷p5247-动态图完全连通性-lct解法/
题目链接:https://www.luogu.com.cn/problem/P5247
完整代码:
#include <iostream>
#include <unordered_set>
#include <stack>
#include <unordered_map>
#include <cstring>
using namespace std;
#define MAXN 5011
#define MAX_LEVEL 15
struct LCT {
int c[MAXN][2], fa[MAXN], sta[MAXN];
bool r[MAXN];
// subtree_size2 is the sum of the sizes of non-preferred children's subtrees
int subtree_size[MAXN], subtree_size2[MAXN];
struct Tag {
// Only stores edges of this level.
unordered_set<int> edges;
int tag;
unordered_set<int> tagged_non_preferred_children;
} tag_tree[MAXN], tag_non_tree[MAXN];
void update_tag(Tag tags[], int x) {
if (!tags[x].edges.empty()) {
tags[x].tag = x;
} else if (tags[ls(x)].tag) {
tags[x].tag = tags[ls(x)].tag;
} else if (tags[rs(x)].tag) {
tags[x].tag = tags[rs(x)].tag;
} else if (!tags[x].tagged_non_preferred_children.empty()) {
tags[x].tag = tags[*tags[x].tagged_non_preferred_children.begin()].tag;
} else {
tags[x].tag = 0;
}
}
void new_non_preferred_child(int x) {
if (fa[x] == 0)
return;
subtree_size2[fa[x]] += subtree_size[x];
if (tag_tree[x].tag)
tag_tree[fa[x]].tagged_non_preferred_children.insert(x);
if (tag_non_tree[x].tag)
tag_non_tree[fa[x]].tagged_non_preferred_children.insert(x);
}
void delete_non_preferred_child(int x) {
if (fa[x] == 0)
return;
subtree_size2[fa[x]] -= subtree_size[x];
if (tag_tree[x].tag)
tag_tree[fa[x]].tagged_non_preferred_children.erase(x);
if (tag_non_tree[x].tag)
tag_non_tree[fa[x]].tagged_non_preferred_children.erase(x);
}
inline int& ls(int rt) {
return c[rt][0];
}
inline int& rs(int rt) {
return c[rt][1];
}
inline bool not_splay_rt(int x) {
return ls(fa[x]) == x || rs(fa[x]) == x;
}
inline int side(int x) {
return x == rs(fa[x]);
}
void Init(int n) {
// Initially every node is a tree by itself.
// memset all to 0.
for (int i = 1; i <= n; ++i) {
subtree_size[i] = 1;
}
}
inline void pushr(int x) {
swap(ls(x), rs(x));
r[x] ^= 1;
}
inline void pushdown(int x) {
if (r[x]) {
if (ls(x))
pushr(ls(x));
if (rs(x))
pushr(rs(x));
r[x] = false;
}
}
inline void __pushup(int x) {
update_tag(tag_tree, x);
update_tag(tag_non_tree, x);
subtree_size[x] = subtree_size[ls(x)] + subtree_size[rs(x)] + 1 + subtree_size2[x];
}
// At first x is not in its tagged_non_preferred_children
inline void __pushup_splay_rt(int x) {
__pushup(x);
new_non_preferred_child(x);
// No need to update tag[fa[x]], because if it was in this subtree, then it is still in this subtree.
}
// tag[x] is not updated.
void __rotate_up(int x) {
int y = fa[x], z = fa[y], side_x = side(x), w = c[x][side_x ^ 1];
fa[x] = z;
if (not_splay_rt(y))
c[z][side(y)] = x;
if (w)
fa[w] = y;
c[y][side_x] = w;
fa[y] = x;
c[x][side_x ^ 1] = y;
__pushup(y);
}
// tag[x] is not updated.
// The original splay root is removed from its father's tagged_non_preferred_children.
void __splay(int x) {
int y = x, top = 0;
while(1) {
sta[++top] = y;
if (!not_splay_rt(y))
break;
y = fa[y];
}
int to = fa[y];
delete_non_preferred_child(y);
while (top)
pushdown(sta[top--]);
while (fa[x] != to) {
int y = fa[x];
if (fa[y] != to)
__rotate_up(side(x) == side(y) ? y : x);
__rotate_up(x);
}
}
void splay(int x) {
__splay(x);
__pushup_splay_rt(x);
}
void access(int x) {
int ori_x = x;
for (int w = 0; x; w = x, x = fa[x]) {
__splay(x);
delete_non_preferred_child(w);
new_non_preferred_child(rs(x));
rs(x) = w;
__pushup_splay_rt(x);
}
__splay(ori_x);
__pushup(ori_x);
}
int find_root(int x) {
access(x);
for (; ls(x); x = ls(x))
pushdown(x);
__splay(x);
__pushup(x);
return x;
}
inline void make_root(int x) {
access(x);
pushr(x);
}
void __link(int x, int y) {
// If simply fa[x] = y, the complexity might be wrong.
access(y);
pushdown(x);
fa[y] = x;
ls(x) = y;
__pushup(x); // Might be unnecessary
}
inline void link_new(int x, int y) {
make_root(x);
__link(x, y);
}
inline void link(int x, int y) {
make_root(x);
if (find_root(y) == x)
return;
__link(x, y);
}
inline void split(int x, int y) {
make_root(x);
access(y);
}
void cut_existing(int x, int y) {
split(x, y);
fa[x] = ls(y) = 0;
__pushup(y); // Might be unnecessary
}
void cut(int x, int y) {
split(x, y);
if (ls(y) != x || rs(x) != 0)
return; // No such edge (x, y)
fa[x] = ls(y) = 0;
__pushup(y); // Might be unnecessary
}
std::unordered_set<int> take_out_edges(Tag type[], int x) {
access(x);
auto tmp = std::unordered_set<int>();
swap(tmp, type[x].edges);
update_tag(type, x);
return std::move(tmp);
}
void add_directed_edge(Tag type[], int x, int y) {
if (type[x].edges.empty()) {
access(x);
type[x].edges.insert(y);
update_tag(type, x);
} else {
type[x].edges.insert(y);
}
}
void delete_directed_edge(Tag type[], int x, int y) {
if (type[x].edges.size() == 1) {
access(x);
type[x].edges.erase(y);
update_tag(type, x);
} else {
type[x].edges.erase(y);
}
}
void new_tree_edge(int x, int y) {
link_new(x, y);
add_directed_edge(tag_tree, x, y);
add_directed_edge(tag_tree, y, x);
}
};
struct DynamicConnectivity {
struct LCT F[MAX_LEVEL];
unordered_map<int, unordered_map<int, int> > level;
void Init(int n) {
for (int i = 0; (1 << i) <= n; ++i)
F[i].Init(n);
}
// Assume no duplicate edge
void link_new(int x, int y) {
level[x][y] = 0;
level[y][x] = 0;
if (F[0].find_root(x) == F[0].find_root(y)) {
F[0].add_directed_edge(F[0].tag_non_tree, y, x);
F[0].add_directed_edge(F[0].tag_non_tree, x, y);
} else {
F[0].new_tree_edge(x, y);
}
}
bool reconnect(int x, int y, int l) {
F[l].access(x);
F[l].access(y);
if (F[l].subtree_size[x] > F[l].subtree_size[y])
swap(x, y);
while (1) {
F[l].access(x);
int u = F[l].tag_tree[x].tag;
if (u == 0)
break;
auto tmp = F[l].take_out_edges(F[l].tag_tree, u);
for (int v : tmp) {
F[l].delete_directed_edge(F[l].tag_tree, v, u);
F[l+1].new_tree_edge(u, v);
++level[u][v];
++level[v][u];
}
}
y = F[l].find_root(y);
while (1) {
F[l].access(x);
int u = F[l].tag_non_tree[x].tag;
if (u == 0)
break;
auto tmp = F[l].take_out_edges(F[l].tag_non_tree, u);
do {
auto it = tmp.begin();
int v = *it;
tmp.erase(it);
F[l].delete_directed_edge(F[l].tag_non_tree, v, u);
if (F[l].find_root(v) == y) {
if (!tmp.empty()) {
F[l].access(u);
swap(tmp, F[l].tag_non_tree[u].edges);
F[l].update_tag(F[l].tag_non_tree, u);
}
for (int i = 0; i < l; ++i)
F[i].link_new(u, v);
F[l].new_tree_edge(u, v);
return true;
} else {
F[l+1].add_directed_edge(F[l+1].tag_non_tree, u, v);
F[l+1].add_directed_edge(F[l+1].tag_non_tree, v, u);
++level[u][v];
++level[v][u];
}
} while (!tmp.empty());
};
return false;
}
void cut_existing(int x, int y) {
auto it1 = level[x].find(y);
int l = it1->second;
level[x].erase(it1);
level[y].erase(x);
auto& s = F[l].tag_non_tree[x].edges;
if (s.find(y) != s.end()) {
F[l].delete_directed_edge(F[l].tag_non_tree, x, y);
F[l].delete_directed_edge(F[l].tag_non_tree, y, x);
return;
}
F[l].delete_directed_edge(F[l].tag_tree, x, y);
F[l].delete_directed_edge(F[l].tag_tree, y, x);
for (int i = 0; i <= l; ++i)
F[i].cut_existing(x, y);
while (1) {
if (reconnect(x, y, l))
break;
if (l == 0)
break;
--l;
}
}
bool is_connected(int x, int y) {
return F[0].find_root(x) == F[0].find_root(y);
}
};
int main() {
int n, m;
static DynamicConnectivity dc;
scanf("%d%d", &n, &m);
dc.Init(n);
int last = 0;
while (m--) {
int op, x, y;
scanf("%d%d%d", &op, &x, &y);
x ^= last;
y ^= last;
switch (op) {
case 0:
dc.link_new(x, y);
break;
case 1:
dc.cut_existing(x, y);
break;
case 2:
if (dc.is_connected(x, y)) {
puts("Y");
last = x;
} else {
puts("N");
last = y;
}
break;
}
}
return 0;
}