CF786D Rap God
CF786D Rap God
Problem
给定 \(n\) 个点的树,每条边有小写字母,定义 \(str(a,b)\) 是 \(a\) 到 \(b\) 的最短路径上将每条边的字符拼接起来所得的字符串。
\(q\) 次询问点对 \(x,y\),求有多少个点 \(z\) 满足 \(z\ne x,y\) 且 \(str(x,y)>str(x,z)\)。\(n,q\le 2\cdot 10^4\),\(1\le x,y\le n\),\(x\ne y\)。
Solution
Rap God (Codeforces Round 406 Div 1 problem D)
点分治。把询问 \((x, y)\) 挂在 \(x\) 上,一个询问只会被处理 \(\log{n}\) 次。
记当前分治中心为 \(root\)。
考察子树内的一个路径起点 \(x\) 及其某个询问 \((x, y)\) 能对应多少个点 \(z\),使得 \(x, z\) 的最近公共祖先为 \(root\) 并且 \(str(x, y) > str(x, z)\)。
由于 \(x \to z\) 的路径必然经过 \(root\),所以可以把 \(x \to z\) 拆分为 \(x \to a\) 和 \(root \to z\),其中 \(a\) 是 \(root\) 在 \(x\) 方向上的子节点。
我们可以先判定 \(str(x, a)\) 与 \(str(x, y)\) 的等长前缀 \(str(x, b)\) 的关系:
- 若 \(str(x, a) > str(x, b)\):则没有能对当前询问产生贡献的 \(z\)。
- 若 \(str(x, a) < str(x, b)\):则有 \(sz_{root} - sz_{a}\) 个点 \(z\) 能对当前询问产生贡献。
- 若 \(str(x, a) = str(x, b)\):则需要比较 \(str(root, z)\) 与 \(str(x, y)\) 的后缀 \(str(c, y)\)。
前两种情况是容易的,难点在第三种情况。
考虑将以 \(root\) 作为起点、子树内的任意点作为终点的所有路径拉出来建一棵字典树,同时对每个节点记录以该节点结束的路径条数,并假设子节点按照入边字母大小从左到右排列。
则有一个很好想的暴力,在字典树上沿 \(str(c, y)\) 走,并不断加上小于当前字母的所有左侧子树的点权和。
你发现一步一步跳实在是太憨了,于是考虑预处理出字典树的根到每一个节点的跳跃过程中所累加的左子树点权和。
在具体考虑有多少个 \(str(root, z) < str(c, y)\) 时,可以直接二分出 \(str(c, y)\) 能在字典树上能匹配到的最深的节点,只需在最后一步没有出边能与 \(str(c, y)\) 时枚举出边累计子树点权和。
前缀相等后的二分算贡献是算法的瓶颈,二分有一只老哥,由于是树上求字符串哈希值所以还要倍增一下,又上一只老哥。外层再套一只点分治的老哥,总时间复杂度为 \(O((n + Q)\log^{3}{n})\)。
有必要说明一些实现上的问题。
-
预处理每个节点到根的正反哈希值,然后倍增实现快速查询 \(x \to y\) 的前 \(len\) 条边组成的字符串的哈希值。类似地还需要实现快速查询 \(x \to y\) 经过 \(len\) 条边后到达的点、第 \(len\) 条边的字符以及两条路径的最长公共前缀等函数。
-
有一个很烦的事情是,你要消除询问点所在子树对字典树的贡献。
一个好写的做法是,先不消除贡献地整体算一遍,再对每棵子树分别建字典树消除贡献,显然这样复杂度仍然是正确的。
-
为了方便查错,可以把子树内的所有查询存到一个
vector里面统一查询,而不是在递归过程查询。 -
被卡哈希了,我最后还是选择写双模数哈希。

浙公网安备 33010602011771号