lxl 又来讲课的记录
太困难。
P7124
前置知识:Eden 的新背包问题。
这个题做法比较离谱。题意是求子树补不删除莫队。要求操作次数 \(O(n\log n)\)。
考虑类似于线段树分治的结构,如果递归左儿子,就加入右节点信息;如果递归右儿子,就加入左儿子信息。这样我们能在 \(O(n\log n)\) 次操作种算出每个叶子在序列中代表的位置的补。
放到树上,我们用轻重链剖分把这个树剖成一些链。首先我们考虑重链上的元素如何处理。考虑如果递归到重链顶的时候(钦定此时已经算出以该重链顶为根的子树补),那么对于重链上的每一个点,我们只需要加入其到重链顶路径上的所有点和所有轻儿子。这个东西均摊下来是 \(O(n\log n)\) 的,因为一个点只会加入该点到根路径的轻边个数次,也就是 \(O(\log n)\) 次。
然后我们考虑轻子树怎么整。这个时候我们就能类似于上面 Eden 的新背包那样做了,但是此时轻子树带权,所以我们建出哈夫曼树。
哈夫曼树就是最优的带权二叉树,每向上两层子树大小加倍。其构造方法就是每次选叶节点权值和最小的两棵子树拉出来合并成一棵子树,一直到最后只剩一棵树。
考虑对于每个重链都这么做,分治哈夫曼树同样是 \(O(n\log n)\) 的。接下来就是把哈夫曼树通过轻边连起来,具体就是某重链的哈夫曼树的根和其父亲的叶子相连。易证该树高为 \(O(\log n)\)。直接分治,然后遇到树上节点或重链顶的根就对应操作即可。时间复杂度 \(O(n\log n)\)。
P2325
树分块前置题。定义首都为界点。
考虑自底向上维护一个栈。记录刚遍历到该点的栈的大小。如果要遍历子树的时候栈的大小增量大于等于 \(B\),那么我们就合并栈内的元素为一块。注意到最后不能只剩一个大小小于 \(B\) 的东西在栈里,所以我们最后把这个东西和我们分出的最后一个连通块合并,就能满足题中所有条件。时间复杂度 \(O(n)\)。
然后 lxl 就给我们普及了一个比传统分讨来分块更好的办法,即 P2325 + 建虚树,每次把虚树上的一条边所对应的连通块找到就能完成每块包含界点最多 \(2\) 的一种分块构造(top cluster,界点为 \(2\) 是为了好打 tag)。但是实际上好像有比这个分块方法更简单的方法(
然后是几个非洛谷题。注意到可能有用的只有一个 trick。考虑如何找到一个节点的祖先子树内第一次包含某种颜色。这个东西就是对每一种颜色建 set(dfn 桶),然后直接求这个东西的 dfn 前驱和该点的 LCA,还有其 dfn 后继和该点的 LCA,取深度更浅的即可。查询或修改的时间复杂度均可以做到 \(O(\log n)\)。
P8204
这个东西是构造有向邻域的不删除莫队。
首先 top cluster 分块。我们有一种比较简洁的方式找到一种 top cluster 分块的界点构造。每次遍历子树,如果子树内所有未分块元素个数大于 \(B\) 或者子树内有超过 \(2\) 个界点,那就在这个地方放个界点。暂时没有想到如何把界点转为完整分块的一种简洁方式,但是这个题只需要界点间的路径就行了。(好像只需要 DFS 一遍只有一个界点的连通块再分一次块就行了/yiw)
UPD:其实不用完整分块,这样分出来对于只有一个界点的连通块就可以暴力跳父亲维护。
记录出现在路径上的询问,不出现在路径上的询问大小都是 \(O(B)\) 的,可以直接处理。
然后对于每个块把路径上的询问全部按深度排序,然后用类似于序列上的回滚莫队做法,先拓展右区间,然后每次查左区间然后直接撤销。这样我们就能和回滚莫队做到一样的询问次数。
P4475
大家都说是 KDT 板子题,但是我不会 KDT。所以我用 lxl 讲的平面分块来做了。(熬到凌晨接近 3 点才过,简直简直。)
对平面分成 \(\sqrt n\times \sqrt n\) 块(边长 \(\frac{M}{\sqrt n}\),但是注意这个题不是完全随机,\(M\) 可能会变),根据随机,每一块内期望只有 \(O(1)\) 个点,所以我们可以直接暴力查与线有交的 \(O(\sqrt n)\) 个区间,然后维护前缀后缀和即可。时间复杂度 \(O(m\sqrt n)\)。
hpi 我就暂时不写了。
P8529
真的是什么都能出莫队。这个题是半平面莫队。
我们先随机撒 \(B\) 个点,对于每个询问向上平移至最近的一个关键点,那么每个点期望有 \(O(\frac{n}{B})\) 个询问。考虑做一遍旋转扫描线的代价是 \(O(n)\) 的。每次处理跨点的询问也是 \(O(n)\) 的,我们取 \(B=\sqrt n\),所以我们得到了总代价为 \(O(n\sqrt n)\) 的做法。
中间有一些巨大卡常难写题暂时先不写。
P4198
这个东西有点离谱。就是你考虑到我想了一个单 log 假做法,发现假了之后在其基础上写了双 log,一直 WA。然后重构代码一遍就过了(
这个东西是线段树单侧递归板子。我们考虑直接维护区间答案长度和右子树在左子树的基础上的答案。这个时候我们有左子树的最大斜率,我们想要跑到右子树里面算贡献进行合并。这个时候我们发现维护区间最大斜率,每次合并我们可以直接递归右子树,如果遍历到某点时左子树最大斜率大于传入的最大斜率,那么就累加右子树在左子树基础上的答案,然后递归左子树,反之直接递归右子树就行了。主要是需要保证右子树内有元素的斜率大于左子树内的最大斜率。
每次合并是 \(O(\log n)\) 的,所以每次修改是 \(O(n\log^2 n)\) 的。
CF1340F
单侧递归的一个练习题。考虑我们在线段树上维护一个形如 )}]}{{(
的序列的前半后半的正反哈希值(反着的时候就是倒着哈希,左括号右括号互换)。同样考虑如何合并两个区间。我们一定是去想抵消中间一部分括号,否则这个东西一定不合法。考虑我们
钦定左区间的左括号数量比右区间的右括号数量多(反之其实同理),我们为了检验是否匹配,等同于我们要求一个区间的左括号的后缀哈希值。
这个东西我们发现我们有单侧递归做法:如果后缀长度小于等于右区间贡献的括号长度,我直接递归右区间;如果大于等于,我们发现查询变成一个区间,这样是不行的,而因为这个地方是匹配的,所以我们把区间变成一个后缀减去另外一个后缀的哈希,不难发现另外一个后缀的哈希就等于右区间的右括号反哈希值。这样我们就有 \(O(\log^2 n)\) 的修改方法了。
考虑查询的时候怎么合并哈希。考虑我们用一条虚链把线段树上的那 \(\log n\) 个区间的信息连起来,然后我们发现我们只需要再写一个函数来递归这个结构来看是否匹配就行了,递归查询方式和上面是一样的。树高是 \(O(\log n)\) 的,所以时间复杂度是 \(O(\log ^2 n)\) 的。
同样的,rupq 暂时不写了。
CF414E
这个是 ETT 弱化版题目(lxl:ETT 不在考纲里,但是平衡树维护欧拉序在考纲里,而且不是 10 级)
考虑用平衡树维护欧拉序(即遍历到这个点和从这个点回溯的时候都把这个点加入到序列里)。我们把这个点的加入点和删除点对应在平衡树上的节点编号都记录下来,方便我们找这个节点当前是序列中的排号为多少的点。具体就是维护一个 fa
标记,每次跳父亲统计前面的点个数。
使用 FHQ-Treap(这玩意我好久没写了)。\(1\) 操作就是求区间的深度最小值,除了它们两者有一个是另外一个点的祖先的情况(这个东西可以特判),然后 LCA 的深度就是深度最小值减一,然后利用 \(dis_{u,v}=dep_{u}+dep_{v}-dep_{LCA}\times 2\) 可以轻松求得答案。
\(3\) 操作就是利用深度变化是连续的,即一个区间内如果深度 \(d\) 满足在该区间最小值最大值之间的话,这个区间内一定有点的深度为 \(d\)。我们等于说找到 dfn 序最大的一个深度为 \(d\) 的点。这个东西是容易的,具体就是如果右子树的最小最大值满足条件就直接递归右子树,然后再看该节点的深度是否为 \(d\),最后再递归左子树。
剩下一个 \(2\) 操作。这个东西其实就是一个区间平移,然后区间修改 dep
。然后我们用 \(3\) 操作的办法找到询问点的 \(h\) 级父亲(区间 \([1,in_{u}]\) 之中的最后遍历到的深度为 \(dep_{u}-h\))的点。然后操作中要求的 最后一个儿子
就是把这个区间平移到 \(out_{u}\) 前面就行了。
P5354
考虑维护函数复合。拆位,然后再把拆的位压回去,容易有 \(O(1)\) 合并标记的方法。然后按位贪心就行了。
HDU 5306
这个是区间取最值的板子题。
每个区间维护最大值和严格次大值,因为区间取最值操作会把最值的数合并,我们每次会以 \(\log\) 的代价合并,所以如果只区间取最值是有均摊的。具体就是对于一个区间,以取 \(\min\) 为例,如果其最大值小于等于修改值,那么不用改;如果修改值在最大值和次大值之间,那么我们直接在这里打 tag,表示该区间内的最大值变成某个数,这个时候如果要维护某些区间信息的话我们就要记录最大值个数了。如果小于等于次大值的话,我们就暴力递归左右两侧来取得新的最大次大值。
因为均摊所以复杂度为 \(O(n\log n)\) 的,据说区间加会让 \(\log\) 个点失去上面的均摊性质,复杂度变为 \(O(n\log^2 n)\),但是不知道为什么。
CF1572F
考虑对于 \(w_i\),我们显然是区间取 \(\min\) 的板子题。但是我们维护的是 \(b_i\)。
考虑我们每次修改值在最大次大中间打标记的时候,我们已经确定这段我们要改什么了,所以此时我们可以直接对 \(b_{i}\) 线段树维护就行了,具体就是修改 \(val+1,mx_{now}\) 区间,减去 \(cntmx_{now}\)。
然后我们有单点修改(同样要在 \(b\) 线段树上改),然后就结束了。非常神秘的是,单点修,区间取 \(\min\) 还是单 \(\log\) 的。注意 \(b\) 线段树有初始值,即所有元素为 \(1\)。
CF793F
课时胡出来了/jy。
考虑扫描线,扫右端点向右,每次就变成一个加入线段的问题。我们发现问题变成了区间大于等于某个数的数全部改成另外一个数(这另外一个数一定大于原来那个数),标记变为维护两个(需要大于的数和变成什么),注意一些合并标记的细节,这个和普通的区间最值有一定区别。
时间复杂度 \(O(n\log n)\)。
CF1919F2
lxl 讲了一个扫描线做法(
考虑换维扫描线,扫序列维,维护时间维。注意到每次修改能变成什么区间取最值之类的问题。
有一个简单得多的办法。考虑把这个东西转成网络流的图,最大流转最小割时候模拟就行了。有性质:A B 所代表边不会同时割掉。我们就只需要维护一个区间左右端点是割 A 还是割 B 的状态,合并区间的时候如果左区间最右侧割的是 B,并且右区间最左侧割的是 A,那么多出了一个 C 的代价。每次就是只是一个单点修改。
接下来时喜闻乐见的减半报警器。
gym 104065 B
这个东西可能是我第一个会的减半报警器,因为下面的题我甚至一时间没有反应过来 \(k=1\) 怎么做。但是这题感觉一堆细节,就没写。
首先把问题转化成需要在线支持包含一个点的所有区间权值减 \(1\),每次需要询问出是那些区间权值减到了 \(0\)。
先考虑如果所有区间都包含一个点,那么我们可能以这个点为界,左右侧都可以排序之后线段树单独维护,但是合在一起就只有根号做法了(二维),所以我们把这个权值分成两半放到左右侧上,如果一侧减到负数,我们就询问另一侧剩余值,然后重新平分。不难发现每次询问到这个区间的某一侧为负数都会使权值至少减半,而询问到的代价是 \(O(\log n)\) 的,会最多询问 \(O(\log V)\) 次,所以这里有一个双 log。
然后考虑怎么把原问题规约到上面那个东西。考虑序列分治。把每次跨过分治中心的区间挂在这个分治中心上,然后就变成上面那个问题了。然后每次修改会动 \(\log n\) 层,每次修改是 \(O(\log n)\) 的,所以仍然是双 log。
最后复杂度就是 \(O(n\log n\log nV)\)。
P7603
lxl:都被卡常了!我:有人最优解总时间 783ms。
减半报警器。注意到每个监控最多监控 \(6\) 个位置。那就平分成 \(6\) 份做就行了。然后对于每个房子维护一个堆,维护一个时间戳然后惰性删除和惰性全局减,每次平分完之后获得的值要加上惰性全局减的标记,然后再加入队列。
为什么这个题我有一个点跑了 8.16s,然后其他点比较快。那我是不是直接打一部分表就能进最优解第一页了。
CF702F
之前讲过的一道题,但是我没补。
首先有倍增值域分块做法,考虑这个东西就是区间 \(>x\) 的数减去 \(x\),考虑我们可以对值域进行 \([2^n,2^{n+1}-1]\) 的分块,然后对于每个线段树都执行暴力减,然后维护区间 \(\min\),因为一个东西最多有 \(\log V\) 次跨块,每次代价是 \(O(\log n)\) 的,然后你会发现包含了 \(x\) 的块是不能直接减的,这个时候我们发现这些点全部跨块,我们就可以把这些点全部找到,然后直接减。时间复杂度是 \(O(n \log n\log V)\) 的。当然,底数不一定是 \(2\),但是不是 \(2\) 的话在处理包含 \(x\) 的块的时候每次最多减 \(B\) 次才跨块。有些时候卡常可能有些用处。
上面那个做法我认为巨大难写,所以我直接就是写平衡树。考虑按剩余钱数从大到小排序维护,每次减钱的是一个前缀,那就直接二分到这个前缀直接减。考虑设减 \(v\),我们可以和原来钱有 \([2v,+\infty]\) 和 \([0,v]\) 的合并起来,然后剩下的 \((v,2v)\) 暴力插入。我们会发现此时点钱数会至少减半,每个钱数最多减半 \(O(\log V)\) 次,那复杂度还是双 \(\log\)。
P4587
大家怎么之前都做过这道题啊。
考虑有一个主席树做法,就是你发现我每次可以暴力拓展,每两次就会去查询一个其中点权会使两次前的区间翻倍的区间,然后可以做到双 \(\log\)。
接下来就是倍增值域分块的单 \(\log\)(但是没有双 \(\log\) 快),大体就是我直接倍增值域分块,维护块内区间 \(\min\) 和区间权值和(ST 表和前缀和即可),然后我到达一个块,如果我权值 \(+1\) 大于等于它的区间 \(\min\),我一定加上了整个区间该块的权值。然后就可以直接做了。
P6781
该来的还是要来啊,这就是那道 rupq。
思路和 CF1340F 基本一致,然后你会发现这个东西维护的不是匹配,而是未匹配的信息,所以我们对于每个节点把中间那两段未匹配上的那段的信息也记录上,就可以单侧递归了。
这个题需要 \(O(n)\) 建树,大概就是暴力把左右儿子设成 i<<1
和 i<<1|1
(如果这两个东西在 \(n\) 以内),然后 DFS(前序)一边整棵树然后 pushup
就行。
离谱的是,写 FHQ(大根堆)用随机权值只有最多 \(68\) 分,然后设成 \(n-i+1\) 就直接过了。
P9996
该来的还是要来啊(第二遍),这就是那道 hpi。
没有想象中卡常,结果是我第一发提交就是 95,还是要看评测机的脸,但是我没调过块长,直接根号冲,赢!
就是对于斜线长成 \
的我们发现每个点上横坐标在其前面(后面)的偏序数量和就是答案,而如果是 /
我们直接容斥一下,求非偏序数量就行了(这个时候注意横或纵坐标相等的也需要统计)。
剩下四道倍增值域分块,最后一道题调了一个晚上,真是傻逼。
P7447
这个是 T-Shirts 的带修版本,你会发现思路和上面我在 CF702F 的思路完全一致。
卡空间。需要底层分块,具体就是设置块长 \(B\),区间长小于等于 \(B\) 的暴力,你会发现 \(B=O(\log n)\) 的时候就完全 OK 了。巨大难写,空间复杂度线性。
P9069
几乎和上一道完全一致,只需要多一个线段树维护负数,然后你发现所有 \(<x\) 的点都会直接变成负数,同样能维护均摊结构。
CF1515I
这个东西可能比较难解释。
考虑直接对重量倍增值域分块,首先这个排序容易变成一个序列。考虑如果我们买了一个和当前的 \(c\) 在同一个块内的物品,那么 \(c\) 的 \(\log_{2}\) 就减一。我们还有另一种减一的方式,就是一直减 \(<2^{\lfloor\log_{2}c\rfloor}\) 的物品,最后把 \(c\) 减下去了。我们发现其实一次减就是要么一直减 \(<2^{\lfloor\log_{2}c\rfloor}\) 的物品减到不能再减,要么是直接减了一部分这样的物品然后直接建一个同块的物品。我想的线段树二分非常复杂,这里有一个比较好的做法。
考虑维护几个值:
- \(lw_{i}\) 表示 \(<2^i\) 的物品重量和,\(lv_{i}\) 则表示价值和。
- \(sc_{i}\) 表示区间 \([l,r]\) 中,我需要多大的初始 \(z\),才能选到一个 \([2^{i},2^{i+1})\) 中间的物品。
这个东西容易 \(O(\log V)\) pushup
。
然后每次询问的时候特判能选完 \(<2^i\) 的物品或是选完 \(<2^{i-1}\) 的所有物品,否则递归两个儿子。不难发现这个东西就是上面的那个线段树二分的集合形式。时间复杂度还是 2log。
P8522
调破防了。
知道上面那个题的做法之后,这个题就简单了。对能力值倍增值域分块,然后同样的,遇到一个同块或是更大的 \(s_i\) 就可以跨块了,图上不好二分,变成倍增,倍增数组维护 \(<2^i\) 的全赢,\(\geq 2^i\) 的全输的能力值增量,也同样维护一个走 \(2^j\) 步内跨块(或是下一次胜利,因为 \(\geq 2^i\) 的任一次胜利都能跨块)的最小 \(z\)。然后询问的时候直接倍增就行了。(要单独处理跨块的情况)傻逼出题人卡巨大常。然后注意倍增的时候不能走进 \(n\),需要特判。