树上莫队
树上莫队,顾名思义,就是到树上做莫队。一般会有两种写法,一种是将莫队正儿八经地搬到树上做;另一种是将莫队搬到树的括号序上做。
树上莫队
算法内容
这是在树上分块跑莫队的版本。考虑到莫队的实现过程,我们需要解决两个问题:
- 如何对树分块,才能保证复杂度?
- 如何移动标记来实现转移?
如何转移
首先我们来解决第二个问题。
观察序列莫队的移动方式,我们发现它其实是将区间端点暴力移动到对应的新端点上;也即,如果我们当前统计了 \((u,v)\) 的答案,需要转移到 \((u',v')\),那么我们可以将 \(u\) 暴力地移动到 \(u'\),将 \(v\) 暴力地移动到 \(v'\),复杂度则由分块来保证。
定义集合的 \(\oplus\) 运算为 \(A\oplus B=A\cup B-A\cap B\),容易验证这个运算具有交换律、结合律和归零律。
考察一下移动过程,我们维护当前路径上的点集 \(S\),那么每次将指针从 \(p,p\in S\) 移走,我们就令 \(S=S\oplus\{p\}\);同理,移到 \(q,q\not\in S\),我们就令 \(S=S\oplus\{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(n\sqrt{q})\)。
例题
SP10707 COT2 - Count on a tree II
把上面说的写一遍就好了,维护不同颜色的数量毫无难度。
括号序莫队
待填坑