算法 | 青训营

120 阅读4分钟

UOJ #284. 快乐游戏鸡题解(长链剖分+单调栈合并)

题面

一番战斗之后,程序猿被计算鸡们赶走了。随着垫子计算鸡一声令下:“追!”,于是计算鸡村全村上下开始乘胜追击。计算鸡们希望在新的一年到来之际给程序猿以重创,出掉这一年的恶气。

可是程序猿一追就走,一走就跑,一跑就无影无踪。计算鸡们开始跋山涉水寻找程序猿的踪迹。快乐游戏鸡跟随大部队走着走着,突然说道:“我好像打过类似的游戏”。

快乐游戏鸡玩过的游戏是这样的:给定一棵 nn 个结点的树,其中 11 号结点是根。每次玩家可以在树上行走,走过一条边需要 11 秒的时间,但只能往当前所在的点的某个儿子走,不能往父亲走。每次游戏需要从 ss 号结点走到 tt 号结点去。

玩家有一个总死亡次数,初始为 00。每个结点上有一个程序猿和一个参数 wiw_i,如果走到结点 ii 的时候,当前总的死亡次数小于 wiw_i,那么玩家就会立刻死亡并回到起点 ss,且该死亡过程不需要时间;如果总死亡次数大于等于 wiw_i,那么玩家就能熟练地对付程序猿从而安然无恙。注意每次游戏时不需要考虑 sstt 上的程序猿。

该游戏会进行若干轮,每轮会清空总死亡次数并给出一组新的 s,ts, t。现在请你对于每一轮游戏输出走到 tt 所需要的最短时间(单位为秒)。

保证每个询问的 ss 可以到达 tt

数据范围

1n,q3e5,1w1e91 \le n, q \le 3e5,\quad 1 \le w \le 1e9

Solution

本题作如下假设

s=起点,t=终点,path=路径上的点,dep=深度,w=死亡次数,subtree=子树,stk=单调栈,sum=st的总死亡次数s = 起点,t = 终点,\\ path=路径上的点,\\ dep = 深度,\\ w = 死亡次数,\\ subtree = 子树,\\ stk = 单调栈,\\ sum = 从s\rightarrow t的总死亡次数
  • Hint1Hint1 考虑单条路径,则有我们在pathpath上会受到阻碍当且仅当满足如下条件,那么对于这一条路径来说,我们想要维护这个信息是可以用单调栈维护的,只要满足以上条件,那就可以把这个点放入stkstk
path[i].dep<path[j].dep&&path[i].w<path[j].w(i<j)path[i].dep < path[j].dep \quad \&\&\quad path[i].w < path[j].w\quad (i < j)
  • Hint2Hint2 考虑ssrootroot的子树,那就会有多条由子树出发到叶子节点的pathpath,那我们可以发现,如果有一个点psubtree(s)&&ppath(st)p\in subtree(s) \&\& p\notin path(s\rightarrow t),但是stk[i].w<p.w&&stk[i+1].dep>p.depstk[i].w < p.w\&\&stk[i + 1].dep > p.dep,那么此时,这个点也是可以放入stkstk的,所以我们要维护的单调栈应该是

    subtree[i].dep<subtree[j].dep&&subtree[i].w<subtree[j].w(i<j)subtree[i].dep < subtree[j].dep \quad \&\&\quad subtree[i].w < subtree[j].w\quad (i < j)
  • Hint3Hint3 考虑如何维护这个stkstk,如果我们每次到一个点uu,然后直接dfsdfs子树获取所有点并尝试放入stkstk的话,虽然能够维护,但是复杂度不对。考虑dfsdfs的过程,如果我们能在搜到一个叶子节点的时候,把当前的stkstk存下来,如果回溯到子树根节点时,再把这些stkstk合并起来,那么如果我们合并方式合理,那就可以优化复杂度,比如用启发式合并可以做到O(nlogn)O(n\log{n}),又由于这个栈是可以以depdep为主关键字的,所以可以用长链剖分,再在回溯的过程进行合并,就能做到O(n)O(n)(还没想通复杂度为什么是O(n)O(n)).

  • Hint4Hint4 考虑统计答案

    sum=(w[i]w[i1])dep[i](ipath(st))sum = \sum{(w[i] - w[i - 1])*dep[i]}(i \in path(s\rightarrow t))

​ 但我们现在是在一个某一个节点上,当前的stkstk中存的是由子树中的节点所构成的,但是tt不一定在 这个stkstk中,所以我们需要用一个二分找到当前stkstk中第一个wmax(path(st).w)w \ge max(path(s\rightarrow t).w),我们令 它为pospos, 但是如果我们一个一个去求和那么时间复杂度又变得不可接 受,这里我们可以用一个 前缀和或者后缀和来维护这个信息,由于我们使用的是栈,由于先入后出的性质我们只能记后缀 和,这样子每次从栈顶拿出元素,后面的和就不会受到影响。

  • Hint5Hint5 考虑求解wmax(path(st).w)w \ge max(path(s\rightarrow t).w),我们直接用树上倍增的方式即可求解

  • Hint6Hint6 关于栈的空间分配问题,由于每个子树下的栈的空间最大都会达到depdep,如果每个都去开一个数组显然空间复杂度会达到O(n2)O(n^2),考虑空间复用。在长链剖分自下往上的回溯过程中,已经被并入长链的轻链是不会再用到的,并且长链上父节点是可以直接继承重儿子的信息的,所以我们只要按照长链剖分的dfndfn序来进行空间复用就行,我们对每一个节点pp可以开一对L[p](栈顶),R[p](栈底)L[p](栈顶), R[p](栈底)表示当前节点stkstk的起始位置和结束位置,优先遍历重儿子,因为单调栈长度最大为dep[p]dep[p],而dfn[p]dep[p]dfn[p]\ge dep[p]所以肯定是空间够用的。

  • Hint7Hint7 计算答案时还有很多细节,加在代码的注释中。

Code