Query on a tree 系列之树剖解法

\(\texttt{Qtree}\) 系列树剖题解(告别 \(\texttt{LCT}\) !)

前言

众所周知 Qtree 系列是 LCT 的题目

但由于蒟蒻平衡树写的fhq,不会Splay,所以蒟蒻不会 LCT,所以蒟蒻决定用树剖写完 Qtree 系列的所有题


QTREE - Query on a tree

直接树剖加线段树,每个点的权值为指向父亲的边权。

注意不要把 LCA 算进去了

Luogu 的 vjudge 只能用 C语言 qq_emoji: tuu ,害得我 CE 了一页

关于本题 C 语言的 \(\texttt{Tips}\) :

  1. 不要用 inline
  2. 头文件选择 #include <ctype.h>
    #include <stdio.h> 就够了
  3. 不要快读,只能用 scanfprintf
  4. 没有 maxswap 函数,要自己手写
  5. 没有 const
  6. 不能 using namespace std;
  7. 没有 string 类型
  8. 函数间不能用 , ,比如不能写 dfs(1), dfs(2);,只能写 dfs(1); dfs(2);

希望你能少 CE 一点

code


QTREE2 - Query on a tree II

路径和可以树上前缀和

求第 \(k\) 个点时先判和 LCA 的关系,如果大于 \(dep_u - dep_{lca}\) ,就找 \(v\)\(len - k\) 级祖先,否则找 \(u\)\(k - 1\) 级祖先

LCA 用重剖,\(k\) 级祖先用长剖,总时间 \(O(nlogn)\)

长剖写错,多测不清空会 RE!!!qq_emoji: tuu

code


QTREE3 - Query on a tree III

线段树维护区间最浅点,查询向上跳就行了

最简单的一题 (要不是 Qtree1 只能写 C语言)

code


QTREE4 - Query on a tree IV

难度陡然上升

先树剖,并对每条重链开一棵线段树

对于线段树上的一个节点 \(p\),其对应区间为 \([L,R]\)(重链上的一条路径)

\(lmx_p\) 表示从 \(L\) 出发,在 \([L,R]\) 间的某个节点拐出重链能到达的最远白点,\(rmx_p\) 同理,\(mx_p\) 表示 LCA 在\([L,R]\) 间的每对白点间距离的最大值

考虑线段树合并

记当前线段树上节点为 \(p\),区间为 \([L,R]\) ,左右儿子分别为 \(ls\) 对应\([L,mid]\)\(rs\) 对应 \([mid+1,R]\)

\(lmx_p=\max\{lmx_{ls},dis(L,mid+1)+lmx_{rs}\}\)

\(rmx_p=\max\{rmx_{rs},dis(mid,R)+rmx_{ls}\}\)

\(mx_p=\max\{mx_{ls},mx_{rs},rmx_{ls}+dis(mid,mid+1)+lmx_{rs}\}\)

接下来考虑边界(线段树叶子节点)

记当前线段树上节点为 \(p\),对应 \([L,L]\)

\(d_i, d’_i\) 表示以 \(i\) 为根的轻子树内,根到白点的最远距离和次远距离,不存在即为 \(-\infty\)

\(L\) 为白点时:

\(lmx_p=rmx_p=\max\{0,d_L\}\)

\(mx_p=\max\{0,d_L,d_L+d'_L\}\)

\(L\) 为黑点时:

\(lmx_p=rmx_p=d_L\)

\(mx_p=d_L+d'_L\)

\(d\)\(d’\) 如何维护?

记当前节点为 \(u\) ,它的一个轻儿子为 \(v\)

由于 \(u\) 在重链上,\(v\)\(u\) 的轻儿子,所以 \(v\) 一定是另一条重链的开端

所以 \(v\) 的 dfn 就是 \(v\) 所在的这条重链所对应区间的左端点

所以从 \(v\) 出发到其子树内最远白点的距离就是这条重链的线段树的根的 \(lmx\)

记这个根为 \(p\)

\(u\) 为白点:\(d_u=\max\{w(u,v)+lmx_p,0\}\)

\(u\) 为黑点:\(d_u=\max\{w(u,v)+lmx_p\}\)

具体可以每个节点开一个 multiset,把轻儿子的 \(lmx+w\)丢进去,如果是白点再丢个 \(0\) ,那么 \(d\) 就是堆顶

对于修改,只需不断向上跳重链,同时删除原来贡献,加入新的贡献

由于一次修改最多更新 \(logn\) 次,所以时间复杂度为 \(O(nlog^2 n)\)

这样写属实有点搞心态,而且三个 \(log\) 有点丑

还有个比较简便的做法

可以添加虚点将树变为二叉树,如图

红点为虚点,红边边权为 \(0\)

这样每个点只有一个重儿子,一个轻儿子,那么 \(d\) 就唯一确定,\(d'\) 必为 \(-\infty\) ,不需要开 multiset 了

code


QTREE5 - Query on a tree V

树剖解法

网上没找到写树剖的题解,蒟蒻试着写了一下树剖,其表现良好,写篇题解为像我一样不会 LCT 的同学提供一种思路

虽然树剖比 LCT 多一个 log,但凭借优秀的常数跑得还是比大部分 lct 更快 (目前排 luogu 最优解第四)


与 Qtree4 类似,每条重链开一棵线段树

对于线段树上的一个节点 \(p\),其对应区间为 \([L,R]\)(重链上的一条路径)

\(lmn_p\) 表示从 \(L\) 出发,在 \([L,R]\) 间的某个节点拐出重链能到达的最近白点(这点与 Qtree4 不同),\(rmn_p\) 同理。(由于询问是关于某个点的信息,不再是全局信息了,所以不需要维护 \(mx\)

转移与 Qtree4 类似

\(lmn_p = \min\{lmn_{ls},dis(L,mid+1)+lmn_{rs}\}\)

\(rmn_p = \min\{rmn_{rs},dis(mid, R)+rmn_{ls}\}\)

\(d_i\) 表示以 \(i\) 为根的轻子树内,根到白点的最远距离,不存在为 \(+\infty\) (这点不同)

这里同样改建二叉树以维护 \(d_i\)\(v\)\(u\) 的轻儿子

\(u\) 为白点:\(d_u=\min\{w(u,v)+lmn_p,0\} = 0\)

\(u\) 为黑点:\(d_u=w(u,v)+lmn_p\)

线段树边界节点 \(p\),对应区间 \([L, L]\)

\(L\) 为白点时:

\(lmn_p=rmn_p=\min\{0,d_L\} = 0\)

\(L\) 为黑点时:

\(lmx_p=rmx_p=d_L\)


以上部分与 Qtree4 的解题思路相仿,略微改动就行,更新颜色也和 Qtree4 一样,不断跳链即可

重点是如何根据我们维护的 \(lmn\)\(rmn\) 得到距 \(u\) 最近白点 \(v\)\(dis(u, v)\)

由于我们维护的是以左端或右端为起点的最小距白点距离,所以只有当 \(u\) 为区间的边界时,我们才能直接用上对应的 \(lmn\)\(rmn\)

所以设当前询问点 \(u\) 所在重链的 \(\text{dfn}\) 区间为 \([L,R]\)

那么 \(u\) 在这条重链上的答案(指白点在以链顶为根的子树内)可由以下两种情况贡献:

  1. \(u\) 作为左端点,贡献 \(lmn\),即 \(lmn_{[dfn_u,R]}\),相当于统计链底到 \(u\) 的答案

  2. \(u\) 作为右端点,贡献 \(rmn\),即 \(rmn_{[L,dfn_u]}\),相当于统计 \(u\) 到链顶的答案

这条重链的答案为上述两种情况取 \(\min\)

而对于白点在子树外的情况,只需不断跳重链,跳到根为止,统计新的重链的答案,跳的时候加上链顶到 \(u\) 的距离,最后再对条重链上的答案取个最小值便是此次询问的答案

由于跳 \(O(logn)\) 条重链,每条重链统计答案需要 \(O(logn)\) 的时间,所以总时间复杂度 \(O(nlog^2n)\)

注意的是这道题所有节点初始为黑色

code


QTREE6 - Query on a tree VI

这题树剖有两种解法,一种是延续 IV、V 的思路,维护端点最值,另一种是每个点单独考虑,这边先写的第二种,第一种后面补

\(b_u, w_u\) 分别表示在以 \(u\) 为根的子树内,如果 \(u\) 为 黑/白 点,\(u\) 的同色最大联通块的大小

初始由于全是黑点,所以 \(b_u = siz_u, w_u = 1\)

询问时,只需向上跳到最远的与 \(u\) 同色的点,它的 \(b\) /\(w\) 即为答案

对于修改,先考虑把 \(u\) 由黑变白

  1. 由于 \(u\) 不再是黑点了,所以有它是黑点所带来的贡献要全部删去。

    \(u\) 所贡献的只能是它的祖先,并且与它在同一个黑色连通块内,或者是在 \(u \rightarrow root\) 路径上第一个白点(贡献这个白点为黑点时的答案),所以找到第一个白点 \(v\)\(fa_u \rightarrow v\) 路径上所有点的 \(b\) 减去 \(b_u\) (相当于 \(u\) 阻断了祖先的黑色连通块与子树内的黑色连通块)

  2. 由于 \(u\) 变为了白点,所以它会带来一些贡献

    \(u\) 所贡献的也是它的祖先,并与它在同一个白色联通块内,或者是在 \(u \rightarrow root\) 路径上第一个黑点,所以找到第一个黑点 \(v\)\(fa_u \rightarrow v\) 路径上所有点的 \(b\) 加上去 \(w_u\) (相当于 \(u\) 联通了祖先的白色连通块和子树内的白色联通块)

这两步都可以用线段树区间加实现

还有个问题就是怎么求 \(u \rightarrow root\) 路径上的第一个 ,这个与 Qtree3 类似

而至于求 \(u \rightarrow root\) 上最远的同色连通块内的点,Qtree3 的操作改一下就行了,即每次查父亲到父亲链顶的第一个异色点,没查到就跳到父亲链顶的位置,否则特判:如果这个异色点是父亲,就返回当前点;否则返回这个点的重儿子。具体看代码

细节比较多,但只要熟悉树剖都能发现

码量和常数似乎都变大了 qq_emoji: kk

code


QTREE7 - Query on a tree VII

还是利用 QTree4 的思想

线段树合并:当前节点 \(p\) ,表示区间为 \([L,R]\)

\(lmx_{p}\) 表示 \(L\) 所在同色连通块内的最大权值

\(rmn_{p}\) 表示 \(R\) 所在同色连通块内的最大权值

光这样显然是合并不了的,考虑合并需要什么

对于当前节点 \(p[L,R]\) ,左右儿子分别为 \(ls[L,mid]\)\(rs[mid + 1, R]\)

显然 \(lmx_{ls}\) 可以贡献给 \(lmx_p\) ,但 \(lmx_{rs}\) 呢?

如果左儿子全是一个颜色,并且与右儿子最左边的颜色相同,那么是不是 \(lmx_{rs}\) 也可以贡献给 \(lmx_p\)

所以设 \(lsiz_p\) 表示左端点所在同色连通块包含区间内的点数(即 \(L\) 在重链上的同色连通块的大小),\(rsiz_p\) 同理

那么转移就出来了:

\(lmx_p = \max(lmx_{ls} , lmx_{rs}[lsiz_{ls} = mid - l + 1][col_{mid} = col_{mid +1}])\)

\(rmx_p = \max(rmx_{rs} , rmx_{ls}[rsiz_{rs} = r - mid][col_{mid} = col_{mid+}])\)

\(lsiz_p = lsiz_{ls} + lsiz_{rs}[lsiz_{ls} = mid - l + 1][col_{mid} = col_{mid +1}]\)

\(rsiz_p = rsiz_{rs} + rsiz_{ls}[rsiz_{rs} = r - mid][col_{mid} = col_{mid +1}]\)

边界情况:设 \(v\)\(u\) 的轻儿子

\(lmx_u = rmx_u = \max\limits_v \{lmx_{rt_v}[col_u = col_v], w_u\}\)

\(lsiz_u = rsiz_u = 1\)

询问:

只需跳到最浅的,同一同色连通块内的祖先,再查,该祖先到其所在链链底这个区间, \(lmx\) 即为答案,但要注意特判跳到根的情况,以及跳时颜色是否相同,细节看代码

修改:

先删除原来的贡献,加入新的贡献,这个只需每个点每个颜色用一个 multiset 维护,记录该颜色连通块内的最大权值即可

然后一边跳重链一边改,同时记录上一个节点(从哪里跳来),和之前的贡献,以便删除贡献和添加贡献,细节在于改颜色时上一个点是否就是修改的点,如果是,在删贡献是要在反色 multiset 里面删(它原来的贡献是存在那里面的)

也是调了好久,拍了一下午

好在树剖依旧稳定输出,拿下最优解第四

code

posted @ 2022-02-13 20:58  After-glow  阅读(129)  评论(2)    收藏  举报