上节我们讲到可以将 DSU 用于数组元素的删除,而有些时候我们删除一个元素时要维护的信息量巨大,那么就可以考虑反向操作。即依次激活每一个元素并合并区间。伪代码如下:
if Active(i) :active[i] = true;if active[i - 1] : merge(i, i - 1);if active[i + 1] : merge(i + 1, i);
适用场景
1. 反向操作、倒序构造 —— 由「删除」变「添加」
正向操作是「删除一个点」—— 破坏区间结构,难以维护。
反向操作是「添加一个点」—— 激活它并与邻接点合并,DSU 能保持区间结构。
因此:
-
将所有操作倒序。
-
初始所有点未被激活。
-
每次添加一个点,更新 DSU.
-
利用 DSU 的段信息倒序回答问题。
适用条件:
-
每次操作对结构影响不可逆或难以正向维护。
-
删除操作逆向后变成容易维护的添加操作。
-
问题允许离线。
2. 维护连续段性质(sum / max / length / 条件计数)
假如题目要求:
-
每个连续 1 段长度是多少?
-
当前所有连续段中最大值是多少?
-
每段的 sum 要维护?
-
是否存在长度为 M 的连续段?
那么可以在 DSU 合并时在根节点维护这些信息。
3. 逐步构造可达性 / 连通区域
有些题出现 “某个条件满足时,该点才可用”,当条件变化时会出现大段的可达区域。
例如:某些区间合并、河流结冰、道路开放类建模。
常用模板
并查集基本操作:
vector<int> fa(n + 1);
iota(fa.begin(), fa.end(), 0);function<int(int)> find = [&](int x) {while (x != fa[x]) x = fa[x] = fa[fa[x]];return x;
};function<void(int, int)> merge = [&](int x, int y) {x = find(x), y = find(y);if (x != y) {fa[y] = x;/* 维护其他信息 */}
};
倒序激活操作:
vector<bool> vis(n + 1); // 记录该点是否被激活
for (int i = n; i >= 1; i -- ) {int pos = p[i]; // 当前激活的是哪一个点vis[pos] = true;/* 根据题目要维护信息 */if (pos + 1 <= n && vis[pos + 1]) { // 合并右区间merge(pos + 1, pos);}if (pos - 1 >= 1 && vis[pos - 1]) { // 合并左区间merge(pos, pos - 1);}/* 根据题目要维护信息 */
}