Atcoder abc351
https://atcoder.jp/contests/abc351
G - Hash on Tree
给定n点树有根数
值数组a[n]
f(u) = a[u] , 如果u是叶子
f(u) = a[u] + prod f(child of u)如果u非叶子
q次操作,每次 修改 a[pos]=val
,每次修改后输出 f(1)
n,q 2e5
4s
1024mb
我的思路
首先这个是收到深度控制的,如果暴力的更新的话
那么一个想法是能否flat掉这个行为
[a,b,c]x[x,y,1] = [0,a*1+b*y,1]
leaf = [0,ai,1]
非叶子 = [ai,1,0]
然而没找到 矩阵方法或者满足“结合率”的东西
如果能有 矩阵或者结合律满足,就是个线段树维护,就好了
其中 上面的乘还可以拆成左右的包裹,总之如果有办法就好了
但是在尝试中发现要得到 a1+by 这种总是伴随秩的下降,没啥办法
第二个想的是上生成方程, 也没有弄出东西
那么最后就是 能否树的结构通过 轻重链 来完成快速计算(q log n)? 也没想到具体维护方法
题解
有很多解法,官方用的一个数据结构叫做 Static top tree.
TLDR
感觉这题解详细但没强调关键,稍微总结一下下面
- 切分方案(点u和它的子树) = 点u的重链 和 点u的轻儿子们
- 线段树的中间状态是一个区间,而这里中间状态有两种
- 第一种状态:重链的
连续的一段
和 这些点的所有亲儿子,这在下面叫做 path cluster - 第二种状态:点u和它的轻儿子的
连续的一段
, 这在下面叫做 point cluster
- 第一种状态:重链的
- 线段树的中间状态是一个区间,而这里中间状态有两种
类似的 | 这里 |
---|---|
用 线段树 管理区间 点修改 和 区间查询 | 用static top tree 管理 静态的tree dp |
线段树的非叶子结点,或者查询结果对应一个区间的状态 | static top tree的非叶子结点,或查询结果对应 一个point cluster/path cluster |
数链剖分 揭露了重轻链 的复杂度优势 | 这里也用了类似优势 |
线段树的状态是 根据你需要设计的 | 这里也是根据你需要设计的,只是因为有两种形式,所以这里有两种状态需要设计 |
线段树的节点状态/查询状态 = 两个子状态的符合 | 这里也需要复合,不过这里复合的子状态来源有5种 |
5种子状态符合
- 单点: vertex(g中的点)->path
- 重链上相邻两段复合: compress(path l,path r) -> path
- 一个节点的 连续轻儿子复合: rake(point l,point r) -> point
- 点u的完整子树 变为 点u父节点的 单个亲儿子: add_edge(path) -> point
- 点u的轻儿子 变成 点u为重链上的一个单点: add_vertex(point,g中的点) -> path
自底向上看就是
- 一个点加入它所属于的重链
- 它自身无轻儿子: compress(compress(…compress(vertex(u))))
- 有轻儿子, compress(compress(…compress(add_vertex(rake(rake(…它的轻儿子们的二叉树)), u))))
- 一个点加入它不属于的上层重链
- 首先它会 按照上面的过程加入到它属于的重链,得到一个 path cluster, 令其为POINT
- 然后 add_vertex(rake(rake(…rake(add_edge(POINT))))) 变成一个 更上层重链的一个单点PATH
- 最后 compress(compress(…compress(PATH)))
1 | function 切割(u 和它的子树): |
本题的point cluster可以表述成单个值
而path cluster可以表述成 ax+b的形式,其中x指代的是 当前path向叶子延伸被截断的重链的部分
目标: 在 线段树上管理一个树的DP
首先,考虑解决这个问题的必要操作
首先考虑O(N)时间解决每个查询, 这很简单,就是树上DP,显然TLE
如果能转化成序列问题
- 修改
a[i]=x
- 查询 $A_1\oplus A_2\oplus \cdots \oplus A_N$
若能转化,则可以segtree维护
观察 1: 对于 perfect binary tree
然后就是 树上dp+记忆化+从节点到根的更新dp,因为 perfect binary tree, 所以深度在log级别,总时间复杂度为 O(q log n)
观察 2: decomposition of tree 和 tree DP的关系
为了展开观察,我们将从另一个角度来解释树状 DP。 事实上,树状图 DP 可以看作是通过合并子树生成有根树的过程,同时保留了一些信息。 现在我们来描述一下这一特性。
在考虑通过合并生成树的过程时,有必要考虑反向操作:分解树的过程。 因此,我们首先考虑分解树的过程。 通过递归执行下面的(1)-(3)直到树上没有边为止,我们就可以把一棵有根树分解成顶点和边。 参见下图。
- (1) 删除一个根顶点。 在这里,不要删除与根顶点相邻的边,因为这些边的端点被认为是一个没有信息的顶点(我们称这样的顶点为虚拟顶点)。
- (2) 通过克隆虚拟顶点,分割出虚拟顶点连接的子树。
- (3) 从每条子树中删除虚拟顶点及其相邻的边。 现在,每个子树都形成了一个普通顶点。
按照相反的顺序,子树逐渐合并,最终形成一棵有根的树。
让我们用函数来表示合并过程。 定义以下四个函数。 在下文中,我们假定顶点附有信息且可区分,但边不附有信息且不可区分。
vertex(v)
: 生成 点vadd_vertex(t,v)
: 1的反向操作, 令 点v 是 树t 的virtual rootmerge(x,y)
: 2的反向操作, 把树x,树y的通过virtual节点合并成一个add_edge(t)
: 3的反向操作, 给有根树t 增加一个 virtual root
1 | vector<vector<int>> g; // 邻接表 |
对于这里来说
1 | using mint = atcoder::modint998244353; |
wow !!! 有点东西,抽象出来之后 这是有点神奇
这样来看, 树状DP 可以看作 通过合并子有根树 来生成一个新的有根树的过程。
- 从叶顶点开始,考虑通过重复执行以下三种操作来合并子树:添加顶点、添加边以及合并根为虚拟顶点的根树。
- 一般树具有以下两个特性,它们阻碍了高效的重新计算
- 长度最坏O(N)
- 子节点最坏O(N)
- 顺便说一下,树形 DP 是一个合并子树的过程,同时保留一些信息
- 合并子树的过程可以按照合并子树的逆操作生成。
这两个事实产生了以下结果:
- 如果我们能以二叉树的形式生成深度为 O(logN)的合并过程,那么树 DP 是否可以高效地重新计算呢?
这种合并过程的实现就是static top tree。
static top tree
注:虽然 Static top tree 的名称是 “Top tree”,但它与 Top tree(狭义)是完全不同的数据结构。 如果你想学习 Top 树,请不要混淆。
- 更确切地说,静态顶层树是 “支持顶层树大部分功能的数据结构的静态版本,这种数据结构是通过管理 Splay 树中链接/切割树上的正常边所维护的信息来实现的”(广义的顶层树)。
- 顺便说一下,我们还可以通过将一棵 Top 树(狭义上)变为静态来构建静态顶树。 有人将这种数据结构称为静态顶树。 我们在此跳过对它的介绍,但如果你有兴趣,请参阅 tatyam 的实现和注释(日语) https://atcoder.jp/contests/joisp2024/submissions/51887735
静态顶树是一棵深度为 O(logN)的二叉树,表示合并子树(实际上不是子树,稍后描述)的过程。
为了说明如何构建静态顶树,我们首先解释合并过程的逆操作,即分解树的过程。 首先,对树应用 HLD 将每条边分为重边和轻边。 (如果您不了解 HLD,请参阅 ABC269 Ex 的题解)。
然后,重复以下步骤 (1)-(4) 直到树上没有边为止。
- (1) 选择与根相连的
重路径
,并删除其中的重边
- (2) 删除根顶点。 这里,不要删除与根相邻的边,因为这些边的端点被视为虚拟顶点。
- (3) 通过克隆虚拟顶点,将虚拟顶点连接的子树分割开来。
- (4) 从每个子树上删除虚拟顶点及其相邻的
轻边
。 现在每个子树都形成了一个普通顶点。
值得注意的是,分解过程中出现的图形不一定是有根树的子树。 也就是说,在步骤(1)中去除边后,可能会出现一个不能被视为有根树的图,这取决于去除边的顺序。 我们借用 “顶树 “这一术语,将分解过程中出现的图形称为cluster。
如图所示,分解过程中会出现以下两种cluster:
- 以非虚拟顶点为根的子树由零条或多条重边连接的cluster
- 形成以虚拟顶点为根的子树的cluster。
接下来,我们按照分解的逆操作,想出一种将cluster并为二叉树形式的方法。在分解过程中,会发生两种cluster合并:合并path cluster和合并point cluster。我们进一步借用 Top tree 的术语,将前者称为 compress,将后者称为 rake。
static top tree的关键是在将cluster合并为二叉树形式时应用技巧,将深度保持在$O(\log N)$。但是,在 (2) 和 (4) 的逆运算中没有技巧的余地;我们可以改进的是逆运算:
-(1)选择一条重路径
并从中移除重边
;
-(3)通过克隆虚拟顶点来分离与虚拟顶点相连的子树。
直观地看,按照以下策略合并集群似乎是一个好主意:
- (1) 的逆操作:在“compose” path culsters时,确保合并过程形成完美的二叉树。
- (3) 的逆操作:在“rake”-ing path clusters时,确保合并过程形成完美的二叉树。
这是一个非常合理的策略,因为一般树的两个属性阻碍了高效的计算:
- 深度$O(N)$
- 子节点$O(N)$
分别通过compressing和raking实现
在这种策略下,最差的的深度可能达到 $O(\log^2 N)$ 我们在此省略细节。原因与使用 HLD + 线段树处理路径查询的简单实现在最坏情况下成本为 $O(\log^2 N)$ 的性质相同。
这就是为什么我们需要另一种ingeuity。“合并cluster以形成完美的二叉树”意味着“合并以使左右子节点具有(几乎)相同顶点的cluster。”事实上,我们可以证明后一种策略允许我们将整个合并过程的深度保持在 O(logN), (我们也省略了细节。原因与在最坏的情况下用 HLD + 线段树在
O(logN) 时间内处理路径查询的想法相同。参考:Nachia 的文章(日语)https://www.mathenachia.blog/mergetech-and-logn/ ,errorgorn 的文章中的“平衡 HLD”一章 https://codeforces.com/blog/entry/104997。)
通过上述技巧,合并cluster的过程可以用深度为O(logN) 的二叉树来表示。
示例代码 https://atcoder.jp/contests/abc351/submissions/52777033(第 5 - 74 行):我们参考了 maspy 和 tatyam 的实现。
原始问题的解
现在我们用static top tree来描述原始问题的解决方案。
合并cluster时需要以下五个函数:
- vertex(v):生成仅由顶点 v 组成的path cluster的函数。
- compress(p, c):执行 (1) 逆操作的函数,即合并path cluster p 和 c(其中 p 更靠近根)。
- add_vertex(t, v):执行 (2) 逆操作的函数,即把顶点 v 分配给path cluster t 的根以形成新的path cluster。
- rake(x, y):执行 (3) 逆操作的函数,即合并point cluster x 和 y。
- add_edge(t):执行 (4) 逆操作的函数,即向path cluster t 添加虚拟根以形成point cluster。
和树型DP一样,该问题可以通过定义树上需要存储的信息,以及构造与这五个函数对应的DP转换来解决。
其中最难的是压缩,即合并path cluster。虽然合并point cluster在某种程度上可以看作是树DP的扩展,可以通过存储相同的信息来执行rake来实现,但合并path cluster意味着合并“特殊形状”的对象。
这里,我们采用 Top tree的术语,将cluster外另一个cluster的顶点旁边的cluster顶点称为boundary顶点。path cluster基本上有两个boundary顶点,一个靠近根,一个离根更远。(当path cluster是根树时会出现例外,其中两个边界顶点指向同一个顶点。)根据问题,人们可以相对轻松地通过关注这两个边界顶点来构建 DP 的转换。
经过适当的观察,可以发现path cluster必须存储 (a,b) 的值,使得“当哈希值为 x 的子树合并到以距离根较远的boundary顶点为根的子树时,以更靠近根的边界顶点为根的子树变为 ax+b”。通过像这样定义path cluster上的信息,compress 可以定义为 affine function 的组合。我们还可以定义其他函数,如下所示:
1 | using mint = atcoder::modint998244353; |
适当抽象一棵静态顶树,任何问题都基本可以通过定义上述五个函数来解决,这跟线段树的抽象只需要两个函数很像吧?
顺便说一下,线段树最初似乎被定义为不仅针对完美二叉树,而且针对一般的平衡二叉树的算法。(参考 https://kmyk.github.io/blog/blog/2020/03/04/segment-tree-is-not-complete-binary-tree/ ,日语)根据这种解释,使用静态顶树更新树DP可以被认为有点类似于线段树。
剩下的就是处理查询;这可以通过适当计算和更新 DP 来完成。
Sample code (line 76 - 134) https://atcoder.jp/contests/abc351/submissions/52777033
Further application of a Static top tree
TODO
代码
https://atcoder.jp/contests/abc351/submissions/58166606
1 |
|
参考+总结
评分是 2920
https://atcoder.jp/contests/abc351/editorial/9899
我看luogu题解好像还是有人用矩阵搞出来了
中文的static top tree似乎叫这个 全局平衡二叉树 https://oi-wiki.org/ds/global-bst/
https://www.luogu.com.cn/article/ef36ensd
相关练习 https://www.luogu.com.cn/problem/P4719
https://oi-wiki.org/ds/top-tree/
hld in atcoder abc 269 ex https://yexiaorain.github.io/Blog/atcoder/abc/269/
https://negiizhao.blog.uoj.ac/blog/4912
可视化 https://maomao9-0.github.io/static-top-tree-visualisation
https://wenku.baidu.com/view/75906f160b4e767f5acfcedb.html
luogu 4211 全局平衡二叉树 https://www.luogu.com.cn/problem/P4211
luogu P3384 【模板】重链剖分/树链剖分 https://www.luogu.com.cn/problem/P3384