Lovi.space

算法

可撤销并查集与线段树分治:在时间维度处理动态连通性

删除边让普通并查集失效时,离线化与回滚如何把动态问题还原成一系列静态问题。

·1 min read
#C++14#算法#并查集#线段树分治

核心洞察:当在线维护太难,不妨把时间展开。把一条边的存在视为一个区间,动态连通性就能被分治成可回滚的静态状态。 有一张设备拓扑图,链路会加入、删除,期间不断询问两个节点是否仍然连通。普通并查集擅长“只加边”:合并后父节点结构被压缩,删除一条边却没有办法知道应该拆开谁。

为什么不能直接路径压缩

路径压缩会修改沿途多个父指针。为了回滚一次合并,你必须准确恢复所有被压缩节点,这使修改记录复杂且代价高。可撤销并查集保留按秩合并,但不做路径压缩;每次合并只记录有限个改变,回滚便能按栈恢复。

struct RollbackDSU {
vector<int> parent, size;
vector<pair<int, int>> history;
int find(int x) { return parent[x] == x ? x : find(parent[x]); }
int snapshot() const { return (int)history.size(); }
};

find 因此是 O(log n) 量级,而不是近似常数;这是为了获得可逆性而主动交换的成本。

一条边在何时存在

给每次加边记录开始时间,删除时得到结束时间 [l, r)。将这条边挂到时间线段树中所有被其区间完全覆盖的节点。遍历到某个线段树节点时,挂在这里的边在该时间段内始终存在,可以安全合并。 递归进入节点前保存快照;处理完子区间后回滚到该快照。这样,左子树的合并不会污染右子树。

阶段 做什么 状态保证
进入节点 合并覆盖当前区间的边 当前区间的静态图完整
到达叶子 回答这一时刻的询问 所有有效边都已加入
离开节点 回滚到快照 兄弟区间互不污染
## 这是一种时间上的分治 每条边只会被放进 `O(log q)` 个线段树节点,每次合并与回滚为 `O(log n)`,总复杂度约为 `O((m + q) log q log n)`。最重要的并不是公式,而是视角转换:删除难以直接维护,于是把“边是否存在”改写为时间区间,再在区间内暂时把它当作永远存在。 ## 实现注意 - 删除不存在的边、重复边要用计数或唯一标识处理。 - 回滚栈应记录“是否真的合并”,否则重复合并会破坏快照深度。 - 查询时间点与区间端点要统一采用左闭右开。 ## 结语 线段树分治教会我们的不是一种冷门技巧,而是一个通用策略:把变化放进时间轴,找到局部稳定的区间,再用可逆的数据结构承接状态。

读到这里,说明你也在认真对待这个问题。

讨论与反应

⌘ K