天天看点

静态链分治进阶

静态链分治,另称 \(\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{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)\)。