树上莫队,顾名思义,就是到树上做莫队。一般会有两种写法,一种是将莫队正儿八经地搬到树上做;另一种是将莫队搬到树的括号序上做。
树上莫队
算法内容
这是在树上分块跑莫队的版本。考虑到莫队的实现过程,我们需要解决两个问题:
- 如何对树分块,才能保证复杂度?
- 如何移动标记来实现转移?
如何转移
首先我们来解决第二个问题。
观察序列莫队的移动方式,我们发现它其实是将区间端点暴力移动到对应的新端点上;也即,如果我们当前统计了 ((u,v)) 的答案,需要转移到 ((u',v')),那么我们可以将 (u) 暴力地移动到 (u'),将 (v) 暴力地移动到 (v'),复杂度则由分块来保证。
定义集合的 (oplus) 运算为 (Aoplus B=Acup B-Acap B),容易验证这个运算具有交换律、结合律和归零律。
考察一下移动过程,我们维护当前路径上的点集 (S),那么每次将指针从 (p,pin S) 移走,我们就令 (S=Soplus{p});同理,移到 (q,q otin S),我们就令 (S=Soplus{q})。所以指针的移动对点集的影响可以用 (oplus) 来描述。
设 (P_{u,v}) 为 (u,v) 路径上的点组成的集合,设 (r) 为树根。不难发现 (P_{u,v}=P_{u,r}oplus P_{v,r}oplus {operatorname{LCA}(u,v)})。
设 (p=operatorname{LCA}(u,v),q=operatorname{LCA}(u',v')),此时我们可以很好地构造具体操作过程:
因此我们不难看出,我们应该对于 (P_{u,u'}) 上除开 (operatorname{LCA}(u,u')) 取异或,对于 (P_{v,v'}) 上除开 (operatorname{LCA}(v,v')) 取异或,此外再对 (p,q) 分别取异或,就可以从 ((u,u')) 转移到 ((v,v'))。
如何分块
不难发现端点在一个(不一定连通的)点集中移动的时候,单次移动复杂度取决于点集的直径。因此,我们需要保证树上的块的直径较小。设这个阈值为 (S),我们需要保证每个块的直径为 (O(S))。
一种简单的分块方法是,我们对树进行 DFS,用栈维护未被分块的点。从某个儿子回溯的时候检查当前栈中点数是否达到了 (S),如果达到了 (S) 就退栈分块;否则我们就将若干个连续遍历的儿子的块合并在一起,直到块大小达到 (S) 再分块;再不济,这些点会和当前节点合并到一个块中,并回溯到上一层。
注意,这里需要将等待合并的儿子块从栈中拿出来,不然会破坏直径性质。
如果我们将儿子块全部留到栈里面,那么相当于按照 DFS 序直接分块,有可能出现如下情况:
这样如果先遍历 (S-1) 的子树,那么这 (S-1) 个点会和 (u) 合并,导致直径过大,复杂度不正确。
实现的时候不需要将剩余的点弹出来,在进入某个节点的时候记录当前的栈底即可。
这样分块,能够保证每个块大致连通(最多只缺一个 LCA),同时可以保证块的大小和直径为 (O(S));并且每个点只会被包含在一个块中。
做莫队的时候将所有询问按照左端点的块编号排序,编号相同的按照右端点的 DFS 序排序,之后按照上面所述的处理即可。
考虑这样做的复杂度,分左端点和右端点:
-
单个块内,左端点移动复杂度为 (O(S)),总次数为 (O(q));
左端点换块,左端点移动复杂度为 (O(n)),总次数为 (O(frac{n}{S}))。
左端点总复杂度为 (O(qS+frac{n^2}{S}))。
-
左端点在一个块内的时候,由于右端点按照 DFS 序排序,所以右端点移动相当于对树进行 DFS 遍历,复杂度为 (O(n)),总次数为 (O(frac{n}{S})),因此复杂度为 (O(frac{n^2}{S}))。
得到复杂度为 (O(qS+frac{n^2}{S})),取 (S=sqrt{frac{n^2}{q}}) 得到最优复杂度为 (O(nsqrt{q}))。
例题
SP10707 COT2 - Count on a tree II
把上面说的写一遍就好了,维护不同颜色的数量毫无难度。
括号序莫队
待填坑