静态链分治进阶
静态链分治,另称 \(\text{dsu on tree}\) ,是一种维护子树信息的强大工具。
但实际上它能做的并不局限于子树信息;
问题引入:
给定一棵树,每个点有三个权值 \(a_i,b_i,c_i\)
对每个 \(i\) 求 \(w_i=\Big|\big\{j|a_j+b_{\operatorname{lca}(i,j)}=c_i\big\}\Big|\)
这个问题有一个基于 \(\text{dsu on tree}\) 的 \(O(n\log n)\) 做法。
先明确在处理每个点 \(x\) 时要求出 \(\operatorname{lca}(i,j)=x\) 的全部点对 \((i,j)\) 之间的贡献。
这样前面的限制有了好看的形式: \(a_j+b_x=c_i\)
那以朴素的 \(\text{dsu on tree}\) 的做法,可以将这些点对分为两部分。
\(1.\) \(i\) 是 \(x\) 的轻儿子 \(y\) 子树内的点,\(j\) 为除 \(y\) 外其他全部儿子子树内的点
这时为了统计 \(w_i\) ,可以让所有的 \(a_j\) 出现的次数开个数组 \(cnt\) 统计。
而统计 \(cnt\) 是天然的一个子树信息,可以在 \(\text{dsu on tree}\) 时同时处理出来 \(x\) 子树内的点的贡献。
但是为了 \(j\) 不在 \(y\) 的子树内,可以先将 \(y\) 子树内的点对 \(cnt\) 的贡献减去,查完后再加回。
由于 \(x\) 固定,每个 \(y\) 子树内的点 \(i\) 直接查 \(cnt_{c_i-b_x}\) 的值即可。
这部分的时间复杂度与 \(\text{dsu on tree}\) 原先复杂度相同,是 \(O(n\log n)\)。
因为都是要将轻儿子子树内全部点统计,重儿子的 \(cnt\) 信息可以直接利用。
\(2.\) \(i\) 是 \(x\) 的重儿子 \(son_x\) 子树内的点,\(j\) 为轻儿子子树内的点
这可以在查轻儿子子树内的点时,考虑对重儿子子树内的点的影响。
对一个轻儿子子树内的点 \(j\) ,这个影响就是在 \(son_x\) 子树内的,\(c_i=a_j+b_x\) 的 \(i\) 点的 \(w_i\) 加一。
如果没有在 \(son_x\) 子树内的限制,直接再开个桶,在每个 \(c_j+b_x\) 处加一,最终对每个 \(i\) 统计 \(c_i\) 被加了多少次。
但即使加上 \(son_x\) 子树内的限制,也可以转化到 \(dfs\) 序上用这个方式实现。
这就可以把之前 \(c_j+b_x\) 的贡献在 \(dfn_{son_x}\) 时加一,在 \(dfn_{son_x}+size_{son_x}\) 时减一。
从小到大枚举 \(dfs\) 序,依次统计 \(dfn\) 所对应的点,作为 \(i\) 被加的贡献。
由于 \(\text{dsu on tree}\) 所带来的这样的操作数是 \(O(n\log n)\) 的,所以这部分时间复杂度同样是 \(O(n\log n)\)。
一些应用
约定:位置 \(i\) 对应的 \(\text{SAM}\) 中的点为 \(rev_i\) ,\(\text{SAM}\) 上的点 \(x\) 对应原序列位置为 \(pos_x\)。(克隆节点 \(pos\) 为 \(0\))
\(\text{U186306}\)
由于要求 \(\operatorname{lcs}(s[1\dots j],s[1\dots i])=s[j+1\dots i]\),考虑扔到 \(\text{SAM}\) 上的 \(\text{parent}\) 树。
那原题中 \(w_i=\Big|\big\{y|pos_y+len_{\operatorname{lca}(x,y)}=pos_x,x=rev_i\big\}\Big|\)
这直接套用上面方法,完全一样。
\(\text{P1117}\)
枚举断点,设每个位置 \(i\) 作为 \(\text{AA}\) 的右端点的值为 \(f_i\) 。
转化到 \(\text{parent}\) 树上就有 \(f_i=\Big|\big\{y|pos_y+[1,len_{\operatorname{lca}(x,y)}]=pos_x,x=rev_i \big\}\Big|\)。
这有别于上面,因为 \([1,len_{\operatorname{lca}(x,y)}]\) 是一段区间而非一个值。
但大体上没有太大差别,其实把之前用的桶 \(cnt\) 改成一个树状数组即可。
-
对于上述第一种情况,仍然是把 \(pos_y\) 插到树状数组内,查询 \([pos_x-len_{\operatorname{lca}(x,y)},pos_x-1]\) 的和。
-
而第二种情况变成一段 \(dfs\) 序内的点,将 \([pos_y+1,pos_y+len_{\operatorname{lca}(x,y)}]\) 的全部值加一或减一。
按 \(dfs\) 序枚举每个点,查询 \(pos_x\) 当前被加了多少。
而作为 \(\text{BB}\) 的左端点的值 \(g_i\),可以将序列反过来以同样的方法再求一次。
最终统计 \(\sum\limits_{i=1}^{n-1}f_ig_{i+1}\) 即可,时间复杂度是 \(O(n\log^2n)\)。
\(\text{P5161}\)
可以将原序列差分后,转化为查询不相交也不相邻的子串对数。
对一个 \(r_1\),考虑其对各个位置的 \(r_2\) 的贡献:
-
也就是说一个 \(r_1\) 对 \(r_2\) 的贡献是一个二维前缀和的形式,
-
反过来,对一个 \(r_2\),\(r_1\) 的贡献为 \(\begin{cases}r_2-r_1-1,r_1\in[r_2-len-1,r_2-2]\\len,r_2\in[1,r_2-len-2]\end{cases}\)
-
对每个轻儿子子树内的 \(r_2\),按上式算出需要区间内的 \(r_1\) 的个数与和,可以用树状数组实现。
-
对重儿子的贡献需要 把能影响 \(r_2\) 的 \(r_1\) 统计出二维前缀和,依然能用树状数组。
\(\text{P4482}\)
设查询区间为 \([l,r]\)。
设 \(x=rev_r\) ,则左侧最长 \(\text{border}\) 的右端点即为 \(w_i=\mathop{\mathrm{Max}}\limits_{y} \ pos_y[pos_y<r][pos_y-len_{\operatorname{lca}(x,y)}+1\leq l]\)。
最终如果 \(w_i<l\) ,则表示不存在 \(\text{border}\) 。
-
对于第一种情况,可以在一个数据结构中将 \(pos_y\) 插到 \(pos_y\) 的位置上,
然后查询位置在 \([1,\min(l+len_{\operatorname{lca}(i,j)}-1,r-1)]\) 中的最大值。
但由于按之前的思路,同时要支持删除某位置上的数的贡献,而 \(pos_y\) 互不相同,可用线段树实现。
这部分时间复杂度为 \(O(n\log^2n)\)。 -
对于第二种情况中,轻儿子对重儿子的贡献,要保证 \(pos_y<r,pos_y-len_{\operatorname{lca}(x,y)}+1\leq l\)
那用一个树状数组套线段树可以实现。
要注意 \(pos_y-len_{\operatorname{lca}(x,y)}+1\) 可能重复,可以对内层线段树下标扩大一些。
由于 \(\text{dsu on tree}\) 会带来 \(O(n\log n)\) 个这样操作,加上树套树要 \(O(n\log^3 n)\)。 -
但事实上这部分还可以不用树套树,仅用一个线段树。
将每个 \(pos_y\) 为下标查到线段树中,对线段树的每个节点,记录这个区间内 \(pos_y-len_{\operatorname{lca}(x,y)}+1\) 的最小值。
这样就能在线段树上二分了,在保证位置 \(<r\) 且区间的这个最小值 \(\leq l\) 前提下,尽量往右查。
最终查到的位置,就是最大 \(pos_y\)。
但操作中存在删除,可以在叶子节点上开个可删堆查动态最小值,总时间仅为 \(O(n\log^2n)\)。
但这并不代表树套树没用,它能解决一个更强的问题:
\(\text{U186697}\)
仍然设 \(x=rev_r\) ,则答案为 \(\sum\limits_{y}[l+k-1\leq pos_y<r][pos_y-len_{\operatorname{lca}(x,y)}+1\leq l]\)
然后发现这用上面的方法几乎完全一样。
-
第一种情况在甚至不用线段树,直接树状数组,在 \(pos_y\) 的位置 \(+1\)。
然后查询的范围为 \([l+k-1,\min(l+len_{\operatorname{lca}(x,y)}-1,r-1)]\) 的和。
这部分仍然是 \(O(n\log^2 n)\)。 -
第二种情况,重儿子的贡献的限制为 \(l+k-1\leq pos_y<r,pos_y-len_{\operatorname{lca}(x,y)}+1\leq l\)
由于是查询个数和,线段树上二分的方法不再可行。
所以仅能树套树,时间复杂度为 \(O(n\log^3n)\)。