为了实现同颜色节点排序、查找前驱后继的操作,我们可以每个颜色开一个 set 保存所有该颜色的节点,这个 set 的比较函数用替罪羊树的标号比较进行重载,这样就能找到当前插入节点相同颜色前驱和后继。替罪羊树发生重构时虽然会修改 tag 的值,但是大小关系一直没变,保证了重载比较的正确性。最近公共祖先可以使用倍增实现。
这样处理之后,问题变成了单点修改,子树求和。前面我们使用替罪羊树维护了 dfs 序,单点修改,区间求和也可以在替罪羊树上完成。因为替罪羊树树高是 \log n 的,我们类似线段树地维护所有节点的子树和即可。查询的时候也类似线段树地求 tag_u 和 tag_{u+n} 之间所有节点的权值和即可。
时间复杂度 O(q \log n),空间复杂度 O(n\log n)。
code
解法2 树上分块
由于没有针对这个做法专门造数据和测试,这个做法相当卡常。我费了不少力气才把代码卡进 2s。
考虑分块。一种朴素的想法是,将树分成 \sqrt n 块,每块用 bitset 把颜色存起来,每次查询的时候只需要将子树下的散点和整块统计即可。但是仅有如此不足以通过本题。主要有两个缺陷:合并各块时 bitset 消耗过大;难以确保分块数量,会被菊花图卡掉。
有了划分方案之后,我们该如何统计信息呢?我们为每个关键点开一个 bitset,记录其子树内所有节点的颜色存在情况。每个节点用一个权值 val 用于记录答案,和上一个解法一样把颜色数转化为对子树内所有节点的 val 的和。另开一个 sum,当 u 为关键点时,sum_u 表示子树内 val 的和;当 u 为非关键点时,sum_u = val_u 。新加入一个颜色为 c 的节点 u,val_u=1,然后找到最近的子树中已经包含颜色 c 的祖先 v,val_v := val_v-1。如果 v 存在,其所在块的关键点一定包含色 c,因此我们可以逐个关键点往上跳,检查 bitset 中是否包含颜色 c,然后 dfs 检查块内节点的子树中是否包含颜色 c。O(\sqrt n) 的块大小保证了暴力查找的复杂度。卡常小技巧是,不要一次遍历整个块,而是从下往上检查,可以避免大约一半的计算量。
查询的时候只需要只需要递归求和子树的 sum,遇到关键点则停止。
总时间复杂度 O(q\sqrt n),最差空间复杂度 O(n^2),没被卡中的话大约是 O(\sqrt n)。牛客实测运行时间是 1758 ms 到 1888 ms 之间。
使用 set 代替 bitset 后,时间 O(q\sqrt n \log n) ,空间复杂度 O(n\sqrt n)。有没有卡常王愿意试一试能不能卡进去。