吉老师线段树
概述
-
吉老师线段树,较正式的名称为 segbeats,是由吉如一发明/整理的使用线段树维护区间最值操作和区间历史问题的方法。
-
本文绝大部分内容都是抄的论文。
区间最值操作
实现原理
-
即 \(\forall i\in[l,r],a_i=\min/\max(a_i,v)\)。
-
我们以 \(chkmin\) 操作为例,\(chkmax\) 操作对称处理即可。
-
考察朴素线段树的区间修改,其依赖于两点:
-
区间整体价值的可快速计算性。
-
懒标记的可合并性。
-
-
显然 \(chkmin\) 操作满足第二条,但不满足第一条,朴素的区间修改方式并不能达到效果(赋值操作乍一看也不满足第一条,但事实上可以暴力重构区间整体价值,而非贡献法)。
-
我们转而考虑进行值域分段,将 \(chkmin\) 操作转化为减操作。具体来讲,我们在线段树节点上维护最大值 \(mx\),严格次大值 \(se\) 和最大值个数 \(cntmx\)。当然,\(sum\) 等常规信息也是要维护的。
-
记当前我们要对 \([L,R]\) 做 \(chkmin(v)\)。首先我们如普通线段树的区间修改操作一般进行区间下放,过程中如果 \(mx\leqslant v\) 则直接返回,否则完成下放过程。
-
对每个到达区间,我们分类讨论:
-
\(mx\leqslant v\)。事实上这种情况在刚才就返回了。
-
\(se<v<mx\)。显然本次修改只会影响到最大值,我们令 \(sum=sum-(mx-v)\times cnt_{mx}\),然后 \(mx=v\),并打上 \(lz=v\) 的标记,其他信息不变,显然这是对的。
-
当 \(v\leqslant se\) 时,无法直接更新此节点的信息,进一步递归。
-
\(v<mx\),且 \(se\) 不存在。相当于第二种情况。
-
-
接下来对这一方式做以复杂度分析。不妨先分析纯最值操作下的复杂度,即只有 \(chkmin\) 一种修改操作。
-
首先我们对这种维护方式做以一定的转化。
-
定义 \(tag_x\) 为线段树节点 \(x\) 的最大值即 \(mx_x\)。然后若 \(tag_x=tag_{fa_x}\),删去 \(x\) 处的标记,注意这一过程是自底向上的。
-
则现在标记有如下几个性质:
-
一条祖孙链上的标记互不相同。不妨假设 \(u\) 为 \(v\) 的祖先,若它们的标记相同,由性质 3 的 \(\geqslant\) 部分(此部分的证明不依赖本条性质)可以推出删标记前 \(u\to v\) 路径上的标记全部相同,于是应当 \(v\) 被擦掉。
-
至多只有 \(n\) 个。不妨认为 \(x\) 处有标记,记其来源为 \(y\),则 \(x\to y\) 的链上除 \(x\) 之外的点都不应有标记,又最后一层只有 \(n\) 个点,所以至多只有 \(n\) 个标记。
-
\(\forall y\in sub_x,tag_x>tag_y\)。显然,否则 \(x\) 子树内的最大值不小于 \(tag_y\)。于是 \(tag_x\geqslant tag_y\),又由性质 1,\(tag_x>tag_y\)。
-
\(a_i\) 的值相当于 \(lf_i\to rt\) 上的第一个标记的值。显然当标记在 \(lf_i\) 上时成立,从而当标记在 \(fa_{lf_i}\) 上时,由 \(lf_i\) 上没有标记,应有 \(tag_{fa_{lf_i}}=tag_{lf_i}=a_i\),归纳可证本性质成立。
-
\(se\) 相当于除 \(x\) 外,\(x\) 子树内的 \(tag\) 最大值。显然,其实可以由性质 3 推出。
-
-
于是,我们的操作可以认为是:
-
找到对应区间,如果 \(mx\leqslant v\) 则返回,否则在对应节点打一个 \(tag=v\)。
-
维护性质。事实上,只要回收掉子树内 \(\geqslant v\) 的标记,就等价于这一目的。这一问题比较复杂,我们详细谈谈:
-
关于性质 2 的维护:性质 2 是归纳的,或者说归纳证明的性质 2 对我们更有利。先给性质 2 加一个“根上一定有 \(tag\)”。
-
显然当 \(n=1\) 时性质 2 成立,进一步考虑把左右子树拼起来加个 \(fa\),显然 \(tag_{fa}=tag_{ls}\) 和 \(tag_{fa}=tag_{rs}\) 至少成立一个,于是两者至少擦一个,于是归纳得证。
-
性质 2 可能不能在子树内维护完全,但可以在子树内维护到对子树成立。而返回时的 pushup 会保证对更大子树成立。
-
充分性:
-
若改完后相同,则说明 \(v\leqslant se\),于是递归,递归证明到 \(n=1\),显然 \(n=1\) 不可能有 \(v\leqslant se\) 也即不相同,得证。
-
打了标记说明 \(mx>v\),那么本来的标记是 \(mx\),现在换了,得证。
-
若 \(\leqslant\),则 \(v\leqslant se\),递归到 \(n=1\),同性质 1,得证。
-
只有原本 \(a_i>v\) 的会受影响,同性质 2。
-
和性质 2 一样是子树归纳的,可以由其他性质推出,略。
-
-
必要性:维护过程中,\(tag<v\) 相关信息显然不变,从而等价于回收 \(\geqslant v\) 的标记,且一定是删了或者换了更小的,否则还得维护,不满足性质 3。
-
-
然后我们定义标记类:
-
一次 \(chkmin\) 产生的标记是同一类。
-
同一个标记下传产生的标记是同一类。
-
不满足前两个条件的标记属于不同类。
-
-
定义标记类 \(\alpha\) 的权值为这一类标记连同线段树的根对应的虚树大小,记为 \(\phi(\alpha)\),定义势函数 \(\Phi(sta)=\sum\limits_\alpha \phi(\alpha)\)。我们进行如下的势能分析:
-
考虑区间下放和打 \(tag\) 对势函数的影响。显然,到达点只有 \(O(\log)\),增加 \(O(\log)\) 个 \(tag\),即势函数只增加 \(O(\log)\)。证明在朴素线段树的区间操作处已经做过了。
-
考虑标记下推,显然只有 \(\phi(\alpha)\) 被增加了 \(1\)。
-
考虑进一步递归对势函数的影响。由上面的证明,我们知道递归的所有到达点 \(x\) 都满足 \(tag_x\geqslant v\),从而访问到的所有点都 \(tag_x\geqslant v\)(由性质 2)...这一步有问题。
-
但至少我们可以证明,进一步递归每至多跑满一条链即花费至多 \(O(\log)\) 的代价,就让势函数 \(-1\)。这对应着一个 \(O(n\log^2)\) 的宽松上界。
-
事实上可以证明进一步递归中每访问一个点就让势函数 \(-1\),于是摊还复杂度为 \(O(n\log)\)。
-
-
综上,修改操作中势函数的总变化量为 \(O(n\log)\),初始时的势函数至多为 \(O(n)\),因为 \(k\) 个点的虚树只有至多 \(2k-1\) 个点(证明见虚树),进一步递归之前的总复杂度也为 \(O(n\log)\),故总复杂度为 \(O(n\log)\),单次操作均摊 \(O(\log)\)。
-
考虑推广到复杂环境,即允许大部分常规操作和最值操作。
-
首先谈谈怎么加减。我们需要一个分配律...我不太确定该怎么说,但我们能看到,\(\min(x,y)+z=\min(x+z,y+z)\),所以大概是加法对最值操作有分配律。故规定总是把加法分配进去,先加后做最值操作,对已有的最值操作懒标记做修改。
-
显然标记类不再能沿用,因为区间加减会改变它(不谈区间赋值,区间赋值在标记类上甚至弱于 \(chkmin\))。
-
但可以修定义:一次区间加减若碰到了某一类标记的一部分,则该类标记分裂为两类;否则该类标记不变。
-
我们把线段树的根从虚树里扔进去。我的意思是,不再额外加进来。
-
...蚌,证不下去,没有充分理解。就算硬凑完,也是 \(O(n\log^3)\)。倒是有一个 promising 的,论文中提到的另一个方向:定义势函数为标记深度和。
-
先放弃证明复杂度吧,背住板子就可以了。
HDU5036 Gorgeous Sequence
-
题意:区间取 \(\min\),区间求最大值,区间求和。
-
数据范围:\(T\leqslant 100,\sum n,\sum m\leqslant 10^6\)。
-
即最值操作板题。注意本题卡常(HDU 的机子太烂了),考虑使用 fread。另外 Vjudge 上的某些翻译不可信,值域下界应为 \(0\) 而非 \(1\)。
WZOI 提高题库 239 Picks loves segment tree
-
看网址似乎总编号是 3050?不管。
-
题意:区间取 \(\min\),区间加,区间求和。
-
数据范围:\(n,m\leqslant 5\times 10^5\)。
-
即复杂环境下的最值操作板题。这可以作为一个 \(\Omega(\log)\) 的例证。
AcrossTheSky loves segment tree
-
不用找了,没地方交。不如去交下一道题。
-
题意:区间取 \(\min\),区间取 \(\max\),区间求和。
-
感性理解一下,这两种操作对对方的影响等价于区间加对对方的影响,因此我们还是有一个 \(O(\log^2)\)。
-
直接复合两者的维护即可。注意区间中只有 \(<4\) 种值时,最大/小值相关信息会重合,需要特别处理。
darkBZOJ #4695. 最假女选手
-
如果 darkBZOJ 也炸了,那么 hydro 的 BZOJ 域里的 #4695. 也是这道题。
-
题意:区间加,取 \(\min/\max\),区间求和、最大、最小值。
-
数据范围:\(n,m\leqslant 5\times 10^5\)。
-
就是只涉及最值操作的常规题目的完全体吧。细节较多,我还没写。
Mzl loves segment tree
-
题意:区间取 \(\min\),区间取 \(\max\),区间加,区间查历史变动次数和。
-
这个...其实不算很“历史问题”。区间加是容易的,对取 \(\min/\max\),利用维护的最大/最小值个数和区间长度,加加减减容易算出有多少个数字变了。
-
至于最值操作转化成的加减操作在下标上不连续的问题,不用管它。反正 pushdown 下去的时候都会解决,连不连续无所谓,在区间内就好了。
ChiTuShaoNian loves segment tree
-
题意:双序列,区间取 \(\min\),区间加,求双序列复合(这里是加)最大值。
-
关于位置分类,即同时为两个最大值/为某一个/都不为,实质目标是维护两者的和,对这四类下标分别进行修改和打标记。\(se\) 当然还是要维护,说实话这信息太多了写起来一定很恶心。
-
事实上,我们是打了四个值作为道标,藉此判断 \(chkmin\) 操作对四类下标分别的效果,四类下标(及其数量)是区间的根本信息,四个值某种意义上只是分界线或者路牌罢了。
-
显然可以分析到 \(O(2^k\log^2)\),这里 \(k\) 是序列数。
Dzy loves segment tree
-
题意:区间取 \(\min\),区间加,区间求 \(\gcd\)。
-
一个很套路的办法是差分,然而区间取 \(\min\)...啊懒了,等我下次翻论文的时候再说吧。
-
这道题就很明显地指出,segbeats 在维护和下标顺序相关的问题时比较无力(这里是逐项差分),因为转化成的加减操作的作用区间不连续。
区间历史最值
实现原理
-
不妨仍然以最小值为例。我们先讨论经典操作下的区间历史最值,然后再将它推广到最值操作上。
-
注意到历史最值和线段树的延迟修改结构在一定程度上是矛盾的。延迟修改可能导致值的峰被延迟过去了。
-
怎么办?引入历史最值相关的懒标记,即,历史懒标记。记录某个点的懒标记的历史最大值,在下传时将它下传,作用于对应的历史最值上。
-
在多操作时可能较为复杂,因为历史最值对于标记的性质有较为严格的要求...具体来说,正常标记下传(显然标记下传严格强于区间修改,我们将区间修改也视作一种标记下传)只需要下传标记作用于信息和下传标记作用于标记,但对于需要维护历史最值的情况,我们需要按如下次序进行下传:
-
下传历史标记(与旧信息结合)作用于历史信息。
-
下传历史标记(与旧标记结合)作用于历史标记。
-
下传标记作用于信息,新信息作用于历史信息。
-
下传标记作用于标记,新标记作用于历史标记。
-
-
合计六步。其中,历史的两步必须在前,当前的两步必须在后,但内部交换顺序无妨,理由在括号里。
-
事实上,似乎可以略去“新信息作用于历史信息”和“新标记作用于历史标记”,因为其严格不强于历史标记下传的效果。这里列出只是为了逻辑清晰。
-
另外,应当指出的是,这一下传在有多种标记时非常严格,多种标记的互相作用可能导致某些历史标记下传是根本不可做的。可以参看 \(\text{UOJ#164. [清华集训2015]V}\) 一题中我的假做法,较有启发性。
-
总体而言,并不提升复杂度,仍然为 \(O(\log)\)。
P4314 CPU 监控
-
题意:区间加,区间赋值,区间求最大值,区间求历史最大值。
-
数据范围:\(n,m\leqslant 10^5\)。
-
直接做就好。注意到赋值操作比较特殊,这玩意具有类似一元的性质(啥跟他结合都是它...)。
-
考虑钦定先加后赋值,赋值后再来加相当于直接加到赋值标记上,记录历史最大加和历史最大赋值,有序下传即可。
-
先赋值后加...这在没有历史最值的时候肯定是可行的,用赋值标记把已有的加标记覆盖掉即可。但在考虑历史最值的时候...注意到加一段-赋值一下-加一段-赋值一下-加一段-赋值一下-...这样的操作并不好处理,因为有多个历史加(被赋值覆盖掉的,而且这里还必须先历史加后历史赋值),我觉得没法做,还是上面的那种比较好。
-
复杂度 \(O(\log^2)\)?应该吧,毕竟赋值操作是可以分裂标记类的。
UOJ#164. 【清华集训2015】V
-
题意:区间加,区间减(至多减到 \(0\)),区间赋值,单点求最值和历史最值。
-
如果没有这个奇怪的减法限制,那么平凡,就是 CPU。但是有这个限制,这就导致我们的历史最大加标记会有各种奇怪的问题。
-
考虑把减法看成一种类似 \(chkmin\) 的操作好了。维护最小值和次小值,分三段考虑,符合 segbeats 的不同类合并,可以 \(O(\log^2)\) 维护。
-
具体地,规定标记次序如下:
-
将标记看成一种 \((mlz,plz,alz)\),即先 \(chkmin\),然后加,然后赋值的东西...
-
对于常规的 tag,\(alz\) 出现代表着前面的寄掉。有 \(plz\) 之后再来 \(mlz\)...把两者合并,若 \(plz>=mlz\) 直接 \(plz-=mlz\),否则合并。毕竟大家本质还是差不多的东西嘛。模仿 CPU,把 \(alz\) 存在时的加减操作直接扔到 \(alz\) 上。
-
对于历史 tag 则比较复杂...显然历史 tag 不关心 \(chkmin\) 到多少,维护历史最大加标记和历史最大赋值标记就好了。
-
-
但是,这一做法是错误的:考虑下传次序,我们显然应该先下传所有历史标记,再下传所有常规标记,次序都是 \(m,p,a\)。简单起见,先把 \(a\) 抛掉,单独考虑 \(m,p\) 两者,若我 \(m-p-m\),则 \(hp\) 应为 \(p\) 的内容,但其显然不能简单下传,若尝试将 \(m\)(不是 \(hm\),是 \(m\),显然 \(hm\) 无意义)先传下去,也是不可行的,因为后面的 \(m\) 会破坏这一次序。
-
显然,一个可行的解决思路是,在计算 \(hp\) 时考虑 \(m\) 的影响。所有 \(m\),都至多只对 \(mn\) 起到修改为 \(0\) 的效果。若区间 \(se\) 存在,则显然 \(hp\) 只要暴力作差就好了,否则本质上是整了个 \(ha\) 出来。
-
显然这未免有些复杂了。事实上,这需要对 \(m\) 到底有没有让 \(mn\) 小于 \(0\)(因而被卡住)分讨,如果其起到这个效果那么其是 \(m\),否则其是 \(p\)。注意随下传的进行,其可能会改变(不再是 \(m\))。然后在计算 \(hp\) 的时候应当对是否有 \(se\) 分讨,有 \(se\) 则为 \(mx+p-m\),否则为 \(p\),计算 \(ha\) 时也要有类似的联立。
-
...我想我们已经从中榨出了足够多的对历史最值标记的理解。我们不再整这个明显过于复杂的东西了,注意到 \(m\) 操作最终总是变成 \(p\) 和 \(a\),考虑如下转定义:\(m(x)=(-x,0),p(x)=(x,0),a(x)=(-\infty,x)\),这里 \((a,b)\) 表示先区间加 \(a\) 再区间与 \(b\) 取最大值。
-
考虑这种标记怎么合并。显然,\((a,b)+(c,d)=(a+c,\max(b+c,d))\)。然而这东西...似乎完全没法处理成历史标记啊?
-
显然此标记本质上是一个分段函数,第一段为 \(k=0\) 的横线,第二段为 \(k=1\) 的分段函数。分段函数合并,是容易的,取最值就好咯,即任意时刻的标记全部合并起来得到的分段函数是我们的历史标记。形式都是统一的,显然可以维护,易证结合律,这里又不求和没必要维护 \(se\) 搞 segbeats 那一套,完了,\(O(\log)\)。
-
也有相对简单一些(不需要分段函数合并)的方法。显然我们在最值操作一节谈到的方法,在本质上将最值操作转化为了加减操作,于是问题退化成 CPU 监控,但需要对最小值和非最小值分别维护一套标记和历史标记(注意,这并非我们闲得,而是因为两者的加标记并不相同,被迫为之)。
51Nod 1768 Rikka with Sequences
-
题意:对 \(a\) 支持单点修,查询 \(\sum\limits_{i=l}^r a_i\) 的历史最小值。
-
数据范围:\(n,m\leqslant 10^5\)。
-
不太传统,对吧?考虑先扔到二维平面上,单点修改变成一个左上矩形修改,即问题现在变成了矩形加,单点求历史最小值。
-
显然基于标记永久化(因而需要交换律)的线套线做不了这个。转而使用 KDT,\(O(m\sqrt{n})\)。
UOJ #169. 【UR #11】元旦老人与数列
-
题意:区间加,区间 \(chkmax\),区间求最小值和历史最小值。
-
显然这还是可以套 V 那道题的复合标记,但这里的问题在于两个分段函数合并后的段数可能增加——炸了,乐。只能用 segbeats 转化成区间加,然后 \(O(\log^2)\) 维护。
P6242 【模板】线段树 3
-
题意:元旦老人与数列再加一个区间求和。
-
显然这就是 segbeats 本来的功能之一,简单处理即可。\(O(\log^2)\)。
区间历史和
区间历史版本和
-
我们先讨论经典操作下的区间历史版本和,然后再将它推广到最值操作上。
-
记和数组为 \(a\),区间历史版本和为 \(b\),设法构造一个数组 \(c\),使得我们可以通过 \(a,c\) 快速计算 \(b\),且 \(c\) 本身的变化量不大。
-
不妨设 \(c=b-t\times a\),这里 \(t\) 为当前时间。显然,当 \(a\) 不变时,\(c\) 不变,且 \(b=ta+c\),容易计算。
-
考虑 \(a\) 被修改会如何。直接算出新的 \(a\),然后用增量法调整之,显然只要在修改中记录当前的 \(t\),这就是可行的。
-
具体来说,记 \(a\) 变动的时间为 \(t\),于是 \(c=b-(t-1)a\),又 \(a'=a+x\),\(b'=b+a'\),故 \(c'=b'-ta'=(t-1)a+c+a'-ta'=(t-1)a+c-(t-1)a'=(t-1)(a-a')+c\),即 \(c'=c-(t-1)x\),这里 \(x\) 为区间变动量和(即 \(a'-a\))。这一区间修改是容易的(不要维护这个复杂的 tag,若区间修为给原数组加 \(v\),则相当于给 \(c\) 的“原数组”,即单点历史版本和加 \((1-t)v\),这个 tag 显然非常可合并)。
-
复杂度 \(O(\log)\)。唔...显然,只要套用 V 中提出的转化,我们就可以以多支付一个 \(\log\) 为代价来支持最值操作。
P3246 [HNOI2016] 序列
- 参看双指针与扫描线-扫描线。事实上,历史版本和是扫描线处理矩形加矩形求和的经典实现方式之一。
P8868 [NOIP2022] 比赛
- 参看双指针与扫描线-扫描线。多序列,真(粗口)恶心。
区间历史最值和
-
即求历史最值数组的和。
-
唔...能不能推广历史版本和的方法?
-
仍然以历史最小值为例,记最小值数组为 \(a\),历史最小值数组为 \(b\),构造 \(c_i=a_i-b_i\)。显然 \(a\) 不变时 \(c\) 也不变,且 \(b=a-c\),容易计算。
-
考虑 \(a\) 变动,若 \(a'<b\) 则 \(a'=b',c=0\),否则 \(c'=c+(a'-a)\),这是显然的。本质上,\(a'<b\),方便起见取成 \(a'\leqslant b\) 好了,代表着 \(a'-a\leqslant b-a=-c\),即 \(c+(a'-a)\leqslant c+-c=0\),诶?
-
实质上就是对 \(c\) 先加 \(a'-a\)(就是修改量),然后和 \(0\) 取 \(\max\)。这一操作我们是熟悉的...由 V,如果其的询问是 \(c\) 的 \(\max\),则我们有一个分段函数合并的 \(O(\log)\) 做法,但显然此时我们构造的 \(c\) 是用来求 \(b\) 的区间和的...如果问的是 \(b\) 的区间最值,反而做不了,因为 \(a_{\max}-c_{\max}\) 不一定恰为 \(\max (a-c)\)。
-
故还是多支付一个 \(\log\) 转化成区间加减解决,由此我们有了一个 \(O(\log^2)\) 的做法。历史最大值也一样,只是 \(chkmax\) 变成 \(chkmin\) 罢了(当前减去历史最大值总应该是非正的)。
-
到这我们谈的都是简单加减。显然,对最值操作,也是可以的,只要拆成加减...诶?注意到我们先前之所以是 \(\log^2\),是因为总体上发出了一个最值操作。这里我们在把对原数组的最值操作拆成加减操作之后,对 \(c\) 数组的最值操作才发出,且可能是各不相同的(转化成的加减操作的变化量取决于原本的区间最值),故会发出 \(O(\log)\) 个最值操作,由此可以看出 \(k\) 层嵌套会导致 \(k-1\) 次拆解和一次 \(\log^2\),实际效果为 \(O(\log^{k+1})\),不过实际上我并不理解怎么继续嵌套(呃呃,题目里就定义一个 \(c\),然后对 \(c\) 求区间历史最值和?这是手动嵌套呀),且我不理解论文里的 \(2^k\) 从何而来。
-
不过,到这里,我们的 segbeats 科技,可以说是点完了。剩下的,主要是在实践中运用和积累经验了。撒花?