前言
我将通过这篇和以前的三篇创作计划讲解洛谷绝大多数的 CDQ 分治和整体二分题目,至于**树别管。
喜报:我在 P3332 [ZJOI2013] K 大数查询 中跑到了次优解,但是帅童把我的代码改了改进行测试把我挤下去,总之记得这题是 me 的次优解。
闲话:
对于来刷无用评论的人:
接下来就切入正题吧。
一、一些**树例题
先来几道简单的**树题目开开胃。
P1383 高级打字机
一眼栈模拟
但是仔细看一看题面:
> 对于前 50%50\%50% 的数据,保证 Undo 操作不会撤销 Undo 操作。
也就是说,对于另外 50%50\%50% 的数据, Undo 操作是会撤销 Undo 操作的。
那么这时候该如何解决呢?
本题中的修改操作只有一个,而 Undo 操作只会影响 Undo 和 Type. 也就是说,如果每次 Undo 和 Type 算作一个新版本,我们可以在 Undo 时直接跳回以前的版本。很容易想到可以用可持久化数据结构来完成。由于可持久化线段树最简单,所以我们可以使用可持久化线段树。把所有字母存在**树的叶子节点即可。
接下来我们想想每个操作要如何实现:Type 操作要放到文本的结尾,我们可以在**树上维护一个 sz 数组,代表每个节点的子树有多少个叶子节点存了字母,在 Type 时二分出当前文本的结尾并在结尾进行 update. Undo 操作是回溯,可以直接用**树的 root 数组实现。**树的 root 数组存了不同版本的线段树其根节点在结构体数组中的下标,当我们要把当前版本回溯时直接新开一个版本然后把根节点设为要回溯的那个版本就行了。Query 是简单的**树上二分查找。
Type 操作和 Query 操作时间复杂度是 O(logN)O(logN)O(logN) ,Undo 操作的时间复杂度是 O(1)O(1)O(1).
Code:
(cin 真香)
下一道
P6166 [IOI 2012] scrivener
这题和上一题简直一模一样,甚至前两个操作的输入都是一样的。但是此题的数据量非常野(**树:我跑 10610^6106 吗?)。而且不愧是 IOI, 样例也是真的大方。
所以你把空间改了改就交上去了吗?
讨论区的大佬们热情地分享了卡常经验:1.可以不开 long long. 2.可以不存左右儿子,让左右儿子跟着函数递归。由于我太蒟蒻了不会后者,开了个 int 卡线过了这一题。
热完身了吗?看看一些紫题。
P3293 [SCOI2016] 美味
前置知识:0-1 Trie 求最大(小)异或值。这个应该所有人都会了,但是这里怕你们说我不负责讲一讲。0-1 Trie 把存进去的每个整数由高位向低位存储二进制值,前面没有就补齐 0。当需要在 Trie 存储的数中找到一个使它与给定数的异或值最大时,从高到低位遍历给定数并从根节点遍历 Trie. 如果当前位为 0 就寻找 Trie 的当前节点有没有值为 1 的儿子,否则查找有没有值为 0 的儿子(与给定数正好相反)。如果有就递归与原数相反的位,否则就只能递归另一个位了。这个贪心确保了最高位的值。
此题只是加了一个 xix_ixi . 怎么做呢?依旧遍历 bib_ibi 的位。如果是 1,我们希望能找到一个此位为 0 的 aia_iai . 如图:
我们要找最小 a 和最大 a 之间有没有值。注意到最小 a 正好就是当前的 ans, 最大值可以算出是 ans+2i−1ans+2^i-1ans+2i−1 ,其中 i 是当前位到最右端的距离。
于是我们只需要求在两个区间 [l,r][l,r][l,r] 和 [ans,ans+2i−1][ans,ans+2^i-1][ans,ans+2i−1] 的约束条件下有没有值即可。如果 b 的当前位是 0,我们也可以计算出对应的左右端点,答案区间为 [ans−x+(1<<i),ans−x+(1<<(i+1))][ans-x+(1<<i),ans-x+(1<<(i+1))][ans−x+(1<<i),ans−x+(1<<(i+1))], 可以自己对照上图画一个图(1000...~1111...)。有两个区间的约束条件,考虑**树。下面的**树使用了一种类似 0-1 Trie
的存储方法,但其实和**树的普通写法是一样的。我原先是求具体有多少个值再判断,但是这个玩意似乎可以直接返回 bool 值然后通过短路的性质优化。于是我就从 2.50s 加速到了 1.80s.
Code:
P4735 最大异或和 是一道类似的题,但是主要是通过可持久化 Trie 或者离线 Trie 实现,基本上是模板题。可持久化 Trie 和**树相似,都是通过函数式编程实现可持久化。下面给出代码,请自主练*其它例题。
二、各种整体二分
树上整体二分
我们前面讲过的整体二分都是在序列上通过数据结构维护的。如果在树上呢?我们似乎可以通过树链剖分达到树上链和统计的效果,但是这样每次修改时间复杂度是 O(log2N)O(log^2N)O(log2N) ,整体二分就变成路边一条了。有没有更快的方法?
给出一道模板。
P4216 [SCOI2015] 情报传递
主要的思路略。由于危险值随时间增加,直接把 i 次询问危险值是否大于等于 k 转化为 1~i-k-1 的时间段有几个点有任务出现,离线处理将每个时间点的询问加入 vector, 然后对应地在树上查询。求多少个人接手是简单的树上差分求链长度。
这里主要讲讲怎么实现高效的树上单点修改和链上和查询。我们先做一次 DFS ,求出每一个节点的入序 st 和出序 ed. 入序可以在 DFS 到这个节点时直接设为 ++cnt, 出序就是在 DFS 完它的子树之后的 cnt. 如果把 szisz_iszi 定义为节点 i 的子树大小则 edi=sti+szi−1ed_i=st_i+sz_i-1edi =sti +szi −1. 如果将叶子节点的出序设为入序,对于其它节点则求子树上的最大出序也是可以的。如下图,蓝表入序,红表出序:
将入序作为树状数组的下标,即可做到单点修改和根节点到给定节点这条链上的权值和。
当节点 u 的权值增加 v 时,将树状数组中区间 [stu,edu][st_u,ed_u][stu ,edu ] 这个区间进行区间增加 v 的操作。查询根节点到给定节点 u 这条链上的权值和(设为 preu\operatorname{pre}_upreu ),preu\operatorname{pre}_upreu 即是所维护的树状数组上 stust_ustu 这个点的权值。为什么呢?我们需要证明当且仅当 u 是 v 的祖先时,stv∈[stu,edu]st_v\in [st_u,ed_u]stv ∈[stu ,edu ]. 求入序的 DFS 是从根节点往下的,因此 u
的子树上所有节点的入序都大于等于 stust_ustu , 根据 edued_uedu 的定义,u 的子树上最大的 ststst 值小于等于 edued_uedu . 因此当 u 是 v 的祖先时,对 u 的修改必定对 v 造成贡献。而如果 u 不是 v 的祖先,如果 u 的遍历顺序在 v 之前,有 edu<stved_u < st_vedu <stv , 否则有 stu>stvst_u > st_vstu >stv , 贡献不会计算给 v.
有了神器之后开始利用。我们计算了 pre\operatorname{pre}pre 值之后可以进行树上差分,设 lcau,v\operatorname{lca}_{u,v}lcau,v 为 u 和 v 的最近公共祖先,faufa_ufau 为 u 的父亲节点,则 u 到 v 这条链的权值和为
preu+prev−prelcau,v−prefalcau,v\operatorname{pre}_u+\operatorname{pre}_v-\operatorname{pre}_{\operatorname{lca}_{u,v}}-\operatorname{pre}_{fa_{\operatorname{lca}_{u,v}}}preu +prev −prelcau,v −prefalcau,v . 于是我们就实现了 O(logN)O(logN)O(logN) 的树上单点修改和链上和查询。本题代码如下:
真正的树上整体二分:P4175 [CTSC2008] 网络管理
这是一道简单的树上整体二分,但是我又饭堂了写了很多遍才过。
树上静态查询链上第 k 小值我在上一篇创作计划中已经讲过了,使用了树上**树。但是如果需要修改,很明显需要树套树,而 Xylophone 自古以来就没写过树套树。考虑整体二分。
有了 DFN 序树状数组的加持这题对你们来说应该比呼吸还简单,依旧带修区间第 k 小模板,只是把树状数组修改的下标变了一下,每次查询查链条上被标记的数有多少而已。
这些题可以用 Tarjan 或者其它算法提前储存 LCA\operatorname{LCA}LCA, 但是太麻烦蒟蒻不会实现直接疯狂剖剖剖。
Code:
P3250 [HNOI2016] 网络
请求的给出和撤销都是整体二分的基本操作。实现链条的修改和单点查询只需要进行简单的树上差分即可。某个服务器出现故障时让你查询这个服务器没影响到的请求的重要度最大值,而且这个题目良心地在每次服务器出问题之后立刻修复,所以 3 操作完全可以看作整体二分的查询。然后把 1,2 操作看作修改,二分答案求重要度最大值,设当前二分到 mid, 执行所有重要度大于等于 mid 的链上修改操作,查询时查询单点权值,并与修改总和进行比较,如果等于总和就说明所有重要度大于等于 mid 的请求都要经过这个点,于是 mid 就美*了,否则这个查询就悠久。这题也能提前存
LCA\operatorname{LCA}LCA, 不过我又没有存。
Code:
扫描线整体二分(偏序整体二分)
P3242 [HNOI2015] 接水果
这是一道极好的神仙题,除了博弈论几乎没有任何其它题的思维有这题巧妙,其天才般的思想和知识点间美妙的融合让所有 OIer 叹为观止。确切地说,这是我此生见过的最巧妙的题,整体二分的实现也不由得让人对出题者产生由衷的钦佩。
注意到这居然是省选题。注意到我不知道风见幽香是谁。注意到这题居然是用盘子接水果而不是水果接盘子,盘子要被水果包含才能装住水果。
树上第 k 小,考虑整体二分,因为我们这篇的标题中整体二分排在最前面。假设我们的盘子是以下蓝色链条,x 和 y 是盘子的两个端点,z 是 x 在盘子上的儿子,能装住盘子的水果的两个端点为 u,v, 其中 depu<depvdep_u<dep_vdepu <depv (如果不是则将两个端点交换), u 和 v 的最近公共祖先是 u:
此时图中红色的部分即是 u 可能的范围,蓝色的部分即是 v 可能的范围。
如果 u 不是 v 的祖先,则:
可贡献的图就如上所示了。因此,询问 (x,y)(x,y)(x,y) 与 lcau,v=u\operatorname{lca}_{u,v}=ulcau,v =u 时 stu∈[1,stz)∣(edz,N],stv∈[sty,edy]st_u\in [1,st_z)|(ed_z,N],st_v\in [st_y,ed_y]stu ∈[1,stz )∣(edz ,N],stv ∈[sty ,edy ] 和当不满足 lcau,v=u\operatorname{lca}_{u,v}=ulcau,v =u 时 stu∈[stx,edx],stv∈[sty,edy]st_u\in
[st_x,ed_x],st_v\in [st_y,ed_y]stu ∈[stx ,edx ],stv ∈[sty ,edy ] 的果子有关。我们注意到这个约束条件很像是矩形,因此可以在每次整体二分中排序,然后用扫描线求出被标记的数有多少个并进行二分。由于扫描线自带一次撤销操作,因此不用在整体二分后再清空树状数组。
这个扫描线没有面积并和周长并里的那么麻烦,只需要在扫到入边时加权,扫到出边时减回去(是个人都会做)。
Code:
成功拿到此题最优解:
P3527 [POI 2011] MET-METEORS
本题是整体二分的经典例题之一。每个国家都是询问,如何求每个国家收到了多少陨石?可以将每个国家的领土用链式前向星存储起来,也可以用邻接表。接下来就是整体二分模板,只需要将询问对象改成国家就行了,可以用 O(Nlog2N)O(Nlog^2N)O(Nlog2N) 的时间复杂度通过本题。本题也有一种单 log 的整体二分解法,看了一晚上没看懂放弃了,我好**看得懂的可以去学*一下。
Code:
整体二分构造单调性序列
例题:P4597 序列 SEQUENCE
很明显这是一道爵士好题,但是不给你谷题号你们应该都会搜错题(
此题有多种解法,比如下凸壳 DP,贪心,整体二分。
1.贪心
贪心操作:从左到右遍历数组,将当前数设为 x, 将以前的序列最大值设为 y. 当 y>xy>xy>x 时,通过 -1 操作把 y 减到 x. 由于使 x=yx=yx=y 的代价相等,因此把 y 减少是最优的。
然后就没了。
这个肯定有什么神秘的原理,下面来讲讲原理。
维护一个大根堆,设每次操作加入一个元素 x, y 为大根堆堆顶,x',y' 为修改后的 x,y. 如果 x≥yx\ge yx≥y 不予操作直接将 x 加入大根堆,否则只要要使 x' 和 y' 都等于同一个数。很明显这个数越小越好,当这个数在区间 [x,y][x,y][x,y] 时,代价是固定的 x−yx-yx−y. 由于这个代价能不花就不花,所以我们把 x' 和 y' 修改到到一个 k 使 k∈[x,y]k\in [x,y]k∈[x,y] 就只需要花 x−yx-yx−y 的代价。如果此时排在 y 前面的最大值 z 大于 y 就不符合单调性了,由于 z
不是堆顶、对单调性有影响,z∈[x,y]z\in [x,y]z∈[x,y], 代价不变,为了不影响单调性将 x' 和 y' 都设为 z 就行了。弹入两个 x 的操作先不谈,此时我们多了两个 z 了,难道不应该把两个 z 弹进堆里面吗?实际上,这两个 z 的数量贡献都是假的,这里称为假值,因为维护单调性,前面的元素 x′=y′=kx'=y'=kx′=y′=k 越小越好,一旦 z 也被后面新加的元素被迫修改,x' 和 y' 就不如设为剩下最大的值 k∈[x,y]k\in [x,y]k∈[x,y], 代价一样而且由于 z 似掉了不会破坏单调性。如果所有 k∈[x,y]k\in
[x,y]k∈[x,y] 都没了,这时候 x' 和 y' 都变成靠得住的真值了,因为你再进行 +1 就会影响后面的元素,再 -1 又会花更多代价。所以这时候将 x' 和 y'(都等于 x) 弹入大根堆。当然这个操作可以简化,由于大根堆的特性只会弹最大的数,因此我们可以先在遍历时就把两个 x 的值弹进去,x' 和 y' 在可用的 k 存在时是不会被弹到堆顶的,相当于滚木,直到所有可用的 k 都没了,这两个值才会有用。
Code:
但是还有另一种做法。
2.整体二分
整体二分需要有符合单调性的询问,但是这个问题没有询问,也没有单调性怎么办?
诶,真的没有单调性吗?
单调性,就在你构造的序列当中!
我们二分的 mid 现在是指当前二分的值域,每次递归我们求出 mid 这个值域所在的位置。如图:
我们计算出对于某个 mid, 最优的位置使得在这个位置之前所有值域小于等于 mid 绝对比这个位置之前所有值域小于等于 mid+1 要好。计算出这个位置之后向这个位置两边递归。如何判断一个位置以前是否一定都小于等于 mid 更好?当一个位置 pos 满足 ∑i=1pos∣apos−mid∣\sum_{i=1}^{pos} \lvert a_{pos}-mid\rvert∑i=1pos ∣apos −mid∣ 最小即可,我们将最小值初始化为 ∑i=1N∣apos−mid−1∣\sum_{i=1}^{N} \lvert a_{pos}-mid-1\rvert∑i=1N ∣apos −mid−1∣.
请结合代码实现和刚才的讲述自主思考。每次递归只用把整个序列遍历一遍即可,时间复杂度 O(NlogN)O(NlogN)O(NlogN).
贪心算法和整体二分的时间复杂度都是 O(NlogN)O(NlogN)O(NlogN), 但是整体二分比贪心还要快。没错,整体二分比直接 STL 优先队列更快(@priority_queue1)。我已用整体二分拿下此题最优解:
Code:
双倍经验:P4331 [BALTICOI 2004] SEQUENCE (DAY1)
本题是一道可并堆好题,可惜蒟蒻我除了可并堆什么都不会。依旧整体二分。求严格上升只需要一开始把所有 aia_iai 减去 i, 整体二分完再加回来就行了。ACGO 也有这题,但是没有整体二分解,我的代码一交上去不停地 WA, 检查发现 ACGO 改了数据,又交了一遍发现还是不行,原来此题要求输出一种方案,需要 Special Judge, 而 ACGO 这个 OJ 是【数据删除】
Code:
由于 ACGO 文章的字数限制,本文将分为上下两部分。下篇原计划和上篇一起出,不过内容过于复杂先咕咕咕一会。反正很快也会出的!
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
↑被腰斩的痕迹
↑宇髓天元的分割线