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