图论 - DSU On Tree
原理
DSU On Tree 是利用树链剖分中重儿子的概念,处理对于每个子树的询问的离线算法。
用伪代码写出来会是这个样子:
1 | def dfs(u, keep): |
我们可以看到,每一个节点都被 dfs 函数遍历了一次。
其次,每一个节点都被 add_subtree_contribute 与 clear_subtree_contribute 通过它到根节点的每一条轻边都遍历了一次。
根据树链剖分,每个点到根节点的轻边数量是 的,所以总的复杂度是 ,其中 是添加一次贡献的复杂度。
我们可以发现,这个算法与莫队具有很多的相似之处,都是增加贡献的同时维护答案。
但是,清除贡献的时候,并不需要这个贡献是可撤销的,只需要把它可能修改过的节点恢复到初始状态即可:因为 dfs 的顺序保证了我们最后会处理一段连续的重儿子。
CF600E
这个题目就是典型的可以用 DSU On Tree 解决的题目了。
我们考虑增加一个点的贡献, 只需要维持一个对颜色的计数数组,同时维护一个出现次数的最大值。
当增加这个贡献后,如果超过了出现次数的最大值,那么它就是新的最大值,并且这个最大值只出现了一次;否则,如果等于出现次数的最大值,那么我们把最大值的出现次数增加一次。
注意这里比较绕,因为需要维护“出现次数的最大值在计数数组中的出现次数”这样一看看起来很抽象的东西。
理解了这一点后,我们就可以确保,每次添加贡献都是 的,所以复杂度就是 。
如上文所说,我们只需要清除贡献时把它可能改动的地方恢复到初始状态即可。
1 | if (!keep) { |
CF1709E
套路地,我们记录 为 点权的异或和,那么 的异或和就会是 ,其中 是 的最近公共祖先。
我们不妨就枚举 ,然后考虑经过 的所有路径,这样枚举是不重不漏的。
当 时, 分布在 的不同子树中。我们可以枚举一个子树中的 ,然后通过某个数据结构查询前面是否出现过 满足 ,然后把 插入到我们维护的数据结构中,作为有可能对后面的 满足条件的 。
如果不同子树中并不存在这样的一个点对,那么我们进一步考虑路径的一个端点可能是 。在这种情况下,在 的所有儿子中,会存在一个节点 满足 。我们只需要在前文提到的数据结构中查询是否存在 即可。
显然这个数据结构可以是 set。
如果我们上面查询到任何一组满足条件的 ,我们肯定需要修改某个节点。
贪心地想,我们只需要把 该为一个 , 足够大,即可保证经过 的所有路径的异或和都不为零。
我们至少也需要一次操作才能把经过 的所有路径的异或和都变成非零的,而根据上面的方案,修改一次就可以保证,所以最小的修改次数就是 。
我们把 修改后,打一个标签,标志它的所有子节点的 都存在一个 ,所以后续进行贡献添加的时候就不再需要考虑 的子树中的所有节点了。
这样做的复杂度就是 ,其中有一个 可以用 uset 去掉,但是由于众所周知的原因,CF 的出题人对 uset,umap 不太友好。
CF741D
据说是这个题目的作者提出了 DSU On Tree。
经过排序能成为回文,这个说法等价于至多只有一个数字出现了奇数次,别的都是偶数次。
我们把边权化为点权:把一个连接父亲和儿子的边的边权放到儿子的点上。
我们假设向量 代表 这个点上每一个字母的出现次数,显然他是一个长度为 的 向量。
进一步地,将 改为到根的所有字母出现的数量,它是一个长度为 的向量。
那么,设 是 的最近公共祖先,那么 路径上每个字母出现次数的向量就是 。
我们只关心每一个数 之后的值,所以这个向量的加法实际上可以用异或表示,即 。
所以我们只需要对每一个点 统计经过它的路径有多少对 满足 即可。
这样, 的取值只有 这 中可能性。
最后,把经过 的答案对子树取一个 就是在 的子树中的答案。
复杂度是 。
ICPC成都 2025 L
由于 是固定的,所以我们先开一个桶 记录下来每个 的出现次数。特别地,对于 单独统计。
我们假设目前记录下来了所有的 ,接下来针对每一个 ,我们进行如下处理:
- 如果 ,那么检查 是否为正。如果是正的,那么将 安排到任意一个 的位置即可,并且 ;如果是非正的,那么我们记录下来这样的失配数量,并且仍然给 。
- 如果 ,我们只需要最后给它们匹配到没有被匹配的 即可。
我们发现,这个失配数量实际上就等于 中所有负数的绝对值加起来。
我们只需要检查这个失配数量,是否能够被 中出现的 配上即可。
下面是一个可能的实现,其中 add 是添加某个 ,del 是删除某个 。
复杂度是 。
1 | auto add = [&](int x) { |