一些数据结构

我对数据结构的掌握,可以说,非常不牛。

PA 2014 Druzyny

\(O(n^2)\) 的 DP 是显然的,考虑优化。
具体地,用 CDQ 分治处理。分治之后我们对于转移的限制可以写成:

  • 左半边的贡献点,可以贡献到右半边询问点的一段区间。
  • 右半边的询问点,可以询问到左半边贡献点的一段区间。

一个转移 \(j\to i\) 合法,当且仅当上面两个条件都成立。
那可以以贡献点 \(j\) 为横轴,询问点 \(i\) 为纵轴。某个 \(j\) 的贡献可以画成 \(1\times k\) 的矩形修改,\(i\) 的查询画成 \(l\times 1\) 的矩形询问。扫描线实现。维护最大值,最大值个数。
总复杂度 \(O(n\log^2 n)\),可以通过。(但好像有一个老哥的做法?)

之前学校模拟赛出了一题。类似,但卡常。

SPOJ11444 MAXOR

直接考虑分块。每 \(B\) 个分一块。

查询的时候,贡献分为:

  • 整块内点对的贡献:
    可以直接考虑预处理出来 \(pre_{i,j}\) 表示 \(x,y\) 都在第 \(i\) 个块到第 \(j\) 个块内,\(s_x \oplus s_y\) 的最大值。\(s\) 是前缀和。
    这件事情可以用可持久化 trie 做,每次从 \(pre_{i,j}\) 推到 \(pre_{i, j+1}\) 就行。
    询问是 \(O(1)\) 的。预处理 \(O(\frac{n^2}B\log V)\)

  • 点对的某一个点,在散块中的贡献:

  • 另一个点在整块中,枚举散块中的点,可持久化 trie 查询。\(O(mB\log V)\)

  • 两个点都在散块中,做普通的 trie,\(O(mB\log V)\)

\(B=\sqrt \frac{n^2}m\) 最优。复杂度 \(O(n\sqrt m \log V)\)

洛谷 P3391 文艺平衡树

不用平衡树,可以根号重构(第一次写根号重构)。
依次存下若干个区间,表示这个区间代表的真实下标区间是 \([l, r]\),正序或倒序。
修改就暴力地查找被其包含的区间,或者和其有交的区间分裂出来一部分(这里细节有点多)。然后暴力 reverse。

注意到每次修改只会多 \(O(1)\) 个区间。每记录了 \(B\) 个区间就重构(就是把维护的下标段,计算到我现在的排列真正长什么样子)。每次 \(O(n)\) 重构,会重构 \(O(\frac m B)\) 次,总复杂度 \(O(mB+\frac {nm}B)\),取 \(B=\sqrt n\) 最优,为 \(O(m\sqrt n)\)

SPOJ2940 Untitled Problem II

显然题意可以转化为,区间加等差数列,区间求最大值。

考虑到区间加等差数列的贡献,可以拆成区间加定值,和,对于 \(i\),令 \(s_i \gets s_i + i\times d\)
这个 \(d\) 我们可以打 tag。询问的时候相当于求 \(\max_i\{(i, s_i) \cdot (d, 1)\}\)

然后考虑对二维平面上的点 \((i, s_i)\) 建凸包,查上面那个数量积的最大值,或者建李超树。分块维护,每个块都弄一个凸包。
区间加定值的时候,整块的答案影响只是定值增量,一个 tag 的事。
区间 \(+i\times d\) 的时候,整块弄 tag,散块暴力重构凸包就好。

如果李超树的话,时间复杂度 \(O(m\frac nB \log V + mB\log V)\)。取 \(B=\sqrt n\) 最优,\(O(m\sqrt n \log V)\)。不知道能不能过。(upd:过不去)(upd:块长调到 50 冲过去了)

凸包的话因为暴力重构是不需要排序的,少半个老哥。

洛谷 P6794 水池

显然 \(0,2,3\) 操作用线段树都很平凡。操作 \(1\) 有点意思。

考虑一个格子 \(x\) 的水被放完,会使它左右的格子的水量呈阶梯状分布。然后这个阶梯的分界线是一段单调的隔板高度。
然后这个隔板是,提取出来这一段区间(不是全局),的后缀 max。
然后你放了一格的水,会影响到左边一段区间(右边同理)。二分算出来。

考虑定义标记 \(tl\) 为,把线段树上这个结点对应的子区间里面的水量,和 \(tl\) checkmin。考虑如何修改和下传。
修改的时候,同时维护一个区间内挡板的最大值,在把一个区间摊到线段树的结点上时,先递归右儿子,同时处理出来右边的挡板最大值。然后向左递归时,把左边的 \(tl\) 和这个最大值取 max,可以达到“后缀 max”的效果。
下传标记同理,先获取右儿子的 max。

至于可持久化,写可持久化线段树就好。

在 ini 大手子的帮助调试下通过了这道题目。

哎你凭什么能过啊,你打一个 tag,要是打到之前的版本里面是不是直接倒闭了。
为什么能过呢?正确性实际上是有的。相信这辈子都不会忘了。

P8987

想了 2h 的根号重构。糖。

这里有一个 key observation,就是假如我原来的 \(a\) 是单调不降的,那么我无论怎么操作都是不降的。而且这时候的 1 操作只需要给一段后缀推平。证明显然。
考虑更普遍的 \(a\)。我假如,某一次给 \(a_i\) checkmin 成功了,那么我就给她加入这个不降的集合 \(S\)。容易发现,在下标对应位置插入就可以维持 \(S\) 不降。
那对于这个 \(S\) 内的贡献,弄一个线段树就好。

接下来的问题就是计算什么时候 \(a\) 会被 checkmin 成功一次。
那么记 \(s_j\) 表示操作 \(1\sim j\) 中 2 操作进行过多少次。\(v_j\) 表示这一次操作如果是 1 那么就是对应值,否则正无穷。
那我相当于,对于每一个 \(i\) 找到最小的 \(j\),满足 \(a_i+s_j\times i>v_j\)

二分。这里只能二分。别瞎往凸包或者李超上直接套。
二分完了之后相当于只建出 \(j\in[1, mid]\) 的直线 \(y=s_j\cdot x-v_j\) 的李超树。蠢一点,可持久化一下。那么这部分做到 \(O(n\log^2 q)\),因为 \(s\)\(O(q)\) 的。

代码(我写的)很长,但是思路非常清晰,好写好调。
当然。还有更聪明的做法。暂时不会。先咕。

P4198

从这里开始单侧递归线段树。单侧递归线段树,适合解决一类前缀限制的问题。
或者拓展一点,在 pushup 的时候再次往下递归,花费一个老哥的代价查一些东西的线段树。

人话题意就是单点修,求全局前缀最大值个数。
对着兔子的 blog 说。

考虑线段树上维护两个值,一个是区间 max,一个是仅考虑区间内值的答案,也即不考虑 \([1, l)\) 的影响。
考虑怎么 pushup。显然左儿子的答案可以直接继承,考虑左儿子的 max 对右儿子的影响。
定义 \(calc\) 函数为计算区间中大于 \(v\) 的前缀最大值个数。
当左儿子最大值 \(lmx > v\) 的时候,右儿子就只会受到左儿子的影响,不用管了,于是返回整个区间的答案减去左儿子的答案。
当左儿子最大值 \(lmx \le v\) 的时候,左儿子就彻底 4 了,递归进右儿子计算即可。
有些取等细节注意一下。

显然每次 pushup 调用一次 calc,而 calc 的复杂度是一个老哥的。所以总复杂度是两个老哥的。

然后这里兔子补充了一点,这种写法需要运算具有可减性。如果多记录一个元素表示右儿子区间内的 prefix max 个数的话,就不需要有可减性。

CF1340F

接着上面的线段树说。
我们每个显然需要维护这个结点涵盖的区间的括号序列中,左右括号消完的最简形式。
然后发现如果删完存在形如 [) 的东西就 4 彻底了。包括她的祖先都没救了。也就是,删完之后,只会剩下一段右括号,拼上一段左括号。

然后考虑线段树上结点信息怎么合并。发现合并的时候,左儿子的左括号,右儿子的右括号中,至少有一方会被删空,否则会出现上面 4 彻底的情况。考虑怎么判断“删空”。
这里就在 pushup 的过程中,记一个 calc 函数,表示获取某一方前缀的 hash 值。
然后查询就是一个类似于线段树二分的东西。我们就当他是 ez 的。
(upd:他妈 ez 在哪,细节一车。自己写的东西假了 100 遍。骂人。)
复杂度两个老哥。

(upd:听说分块写出来完全没有细节。分块的好处是他可以直接平推,不需要 pushup。)

CF1416E

鸽子王补货。
花费 1s 时间写出一个暴力 DP。设 \(f_{i,j}\) 表示前 \(i\) 个,且 \(b_{2i}=j\) 的最小代价。转移是

\[f_{i}(j) \gets f_{i-1}(a_i-j)+1-[2j= a_i] \]

\[f_{i}(j) \gets \min_k f_{i}(k)+2-[2j= a_i] \]

直接对着这个式子优化。考虑 \(f_{i-1}\to f_i\) 会经历什么。
第一个转移式子是形如,(几乎)全局 reverse。第二个式子对于任意的 \(j\) 都生效,看做全局 checkmin。然后全局加法,单点减法。

我们可以维护一个动态开点线段树。对于翻转操作,并不用真的给他翻转。考虑到可以给全局维护一个二元组 \((k,b)\),表示我线段树上下标为 \(x\) 的点,真实存的是 \(kx+b\)
然后这个 checkmin 是可以直接打标记的。因为支持的查询操作是 qmin,是可以下放的。同时注意维护当前真正有效的区间。即 \(f_i\) 的定义域是 \([1, a_i-1]\),超出定义域的打一个加 1000000 的标记毙掉。
复杂度一个值域老哥。好像容易爆空间。代码咕掉。

另外一种方法是用 set 维护。ri 老师牛考虑一个结论:转移点只可能是 \(f_{i-1}\) 的最小点值。证明不难。
set 里面维护的就是这些最小的点值。

考虑第一个转移。区间翻转类似上面的维护。只不过我这里由于是 set 所以可以把一些不合法的区间暴力 erase 掉。
然后考虑什么时候会 checkmin 成功。注意到,由上面的结论,如果我某次整个 set 里面的东西都 4 了,那么他才有机会做 checkmin。然后这个时候他加入的点值是一整段区间。所以我 set 维护的是连续段。

然后我每次最多加入一个连续段,所以暴力 erase 是没有问题的。复杂度 \(O(n \log n)\)。5e5 的 set,信仰跑。

P9986

好玩题。思路来源于 cyffff 的题解。
值域可以离散化成 \(O(n)\) 的,具体地,把 \([1, n]\) 暴力跑一遍,然后只取出过程中有值的位。
然后你就可以写出来一个线段树维护的莫队。时间复杂度 \(O(n\sqrt m\log n)\)。过是不可能过的。
那题解里面有人上手法用压位乱草的。在这里就不说了。

考虑回滚莫队。称莫队排序规则用的块为序列块。
首先这里右端点递增扩展,进位处理暴力做就好,因为总进位数是 \(O(n)\) 的。考虑左端点的扭动。这里因为有撤销所以均摊就 4 了。

把左端点所在的序列块里的元素提取出来。一共有 \(B\) 个对吧,那把她们画到值域上,有 \(B\) 个点。以这些点为右端点,分成 \(B\) 个块。大概就是,如果序列块里有 \(3, 6, 8\),那值域分块就是 \([1, 3],[4, 6],[7,8],[8,n]\)

右端点动的时候,我们维护了一个数组表示真实的二进制值对吧。那么考虑左端点扩展,可能会在块进位。那记下来我到底进了多少位。
然后显然这一个块的进位,只会影响到下一个块的前 \(\log n\) 位。那我用一个 int 存每个块的前老哥位,然后进位的时候只可能往后溢一位,再记录从 \(1+\log n\) 位开始的一个前缀,的最长的 \(1\) 有多少个。

那这个前缀有多少个 \(1\),popcount 就会损失多少对吧。
然后查询的时候直接暴力地在这 \(B\) 个块上,从低位到高位跑一遍。

还有一些细节,比如说我在移动右端点的时候,如果值域块末发生了进位怎么办呐?方便一点的写法就是,不往下一个块进了,和左端点的进位存在一起。
比如说我某一个块的块长要是小于 \(\log n\) 了怎么办呐?实际上是平凡的。

那比如说我怎么处理前缀的 1 的个数?拿个链表就完事。

\(B=O(\frac n{\sqrt m})\) 最优,总复杂度 \(O(n\sqrt m)\)。块长 520 神力/qiang

P12554

额一个傻逼的想法是,二分答案一个 log,判定跑连续段 DP,线段树优化,做到平方 log 方,然后没有任何优化前途。(暴力分没调出来的死掉吧)

那首先认真观察一下切段的形式。有一个结论是,段数一定小于等于 \(3\)。调整法合并,证明是平凡的。
然后这里得出结论,切,就要切出来正负交替的。
(这里的总结是什么?总之就是,多观察有没有诈骗的点,比如重要的 xxx 会不会很多,从而大大简化问题)

那段数等于 \(1\) 平凡,考虑段数等于 \(2\)
那考虑区间总和 \(\ge 0\) 的情况。假设左段为正,右段为负。那左段取的一定是(前(后面也可能是后)缀和的,下略)前缀 max,因为把任意一个和为负的段丢到对面不劣。更进一步,实际上取前缀 max 中最大的。
总和 \(<0\) 的情况自然就是取全局 min。左右段都讨论一下,是简单的。

考虑段数为 \(3\) 的情况。这里发现中间一段的和的绝对值,一定是三段中最大的,要不然不如 merge 起来。
诶诶诶我现在会两个老哥的做法了!不妨设两边的两段为正,中间一段为负。
考虑二分答案,求出来我左区间右端点的最左位置,和右区间左端点的最右位置。然后直接求这个区间里面的最小子段和作为中间一段。正确性显然。

额你稍微动点脑子(这个想不到成傻逼了)。考虑类似段数为 \(2\) 的情形,答案形如【取一个分界点 \(i\),令左段为 \([l, i]\) 的最大的前缀 max,右段为 \([i, r]\) 中最大的后缀 max,两个东西取 min】的最大值。
这东西,“必要性”显然,但是“充分性”没那么显然,也就是,可能会高估答案。也就是,中间的负数段不够大的情况。那此时算出来的东西显然不如只取整个一段优。

那前缀 max 是单增的,后缀 max 是单减的,二分一个分界点下班。一个老哥。
额需要精细实现,比如线段树二分的时候,二分到分界点,取答案的时候要前后加减 eps,之类的。

P14829

都说糖。场上效率太低导致的。
那么官解的做法是线段树上每个结点都是一个虚树,询问点也建到虚树里面去。然后 2log 变成 1log 也是套路的,建虚树用归并排序 + 单调栈。

这里讲另外一个思路。
考虑扫描线,每次【点亮】树上 \(a_i\) 的点。记 \(a\) 中某个数目前出现最后一次的位置是 \(p_i\),那么考虑更新答案,对于一个询问(因为是扫描线,只关心左端点了) \(L,u\),考虑 \(u\) 到根的路径上的每一个点是否可能成为答案。
那一个简单的刻画是,在子树抠掉 \(u\) 所在的那一块中,查询 \(p\) 最大值是否 \(\ge L\)

考虑重链剖分(链分治),对于一段重链,维护其轻子树内的信息。
那每次扫描线加进来点,会更新 \(p\);查询跳重链,可以看做:求一段前缀中,权值大于等于 \(L\) 的最大编号。那直接对每个重链 CDQ 就是三个老哥的。

那对于跳重链的时候,重链底端的点,我是不能直接用轻子树信息的,因为这时候 \(u\) 所在的也是轻子树,但我要把她抠掉。
这样的点只有 \(O(\log n)\) 个,怎么把子树拍到 DFS 序上,放到线段树上求区间 max。因为不是瓶颈,所以随便写了。

我觉得常数不大,因为一个是剖子,一个是 BIT。又有每次修改 \(p\) 的值是递增的,所以好写一点,常数相应也会小。能不能过不知道。

upd: 过了,洛谷最慢点 4.85s,QOJ 最慢点 3.6s。出题人没卡剖子说是。

ARC063F

神秘结论题。题解属于个人记录,不包懂。
但是分治想不到洗洗睡了。

那么我场上认为 \(O(n^2)\) 的暴力是显然的。考虑扫描线,扫描矩形的下边界,然后每次单调栈暴力求出支配区间。
然后这个跟正解相去甚远。

trick 是,遇到这种二维平面求矩形最值啥的,先别急着扫描线,可以看看分治。
考虑对着横坐标分治,求跨过中心 \(mid\) 的最大周长。

考虑对 \(x\) 坐标扫描线,从左到右枚举矩形的右边界。左边界自然也一定是,某个关键点的横坐标。那么考虑线段树的下标记录,左边界为 \(i\) 的最大半周长。
那么在平移右边界的过程中,需要支持区间加(横向长度的扩展)。还要考虑新加进来一个点,新增了一些限制。
然后这个限制显然同样横坐标,只有 \(\ge mid\) 的最小值、\(<mid\) 的最大值有用。

沿用暴力的思路,单调栈可以处理这个问题。具体地,开两个单调栈,可以处理这个限制。
然后不要陷入思维定式:格点在矩形边界上,也是合法的。于是单调栈中每个元素,实际上是她“前驱”的限制。即:

然后考虑移动左边界 \(x\) 的时候,加入 \(x-2\) 的答案。
出栈的时候,会对一些点的贡献砍一刀。形如
减掉就好。

需要注意的是,某个点 \(t\) 的“前驱”并不一定是 \(t-1\),或者剩余栈的栈顶。但是我们如果做一个区间修改,就一定会把贡献减干净。

最后特殊处理一下“相邻点对”。形如

那就做完了。

然后有个优化。结论是,答案的一个下界是 \(2\max(w,h)+2\)。取一个长条即可。所以更优的矩形一定是横向跨过 \(\frac w2\),或纵向跨过 \(\frac h2\) 的。然后就有了一个自带的分治结构。少一个老哥。总复杂度一个老哥。

一个模拟赛题

题意:给定长为 \(n\) 的序列 \(a\),对于一个子区间,定义权值为,考虑其内部所有元素构成的可重集 \(S\),并且考虑集合 \(T=\{x|\exists S' \subseteq S, x=\sum_{i\in S'}a_i\}\),则权值为 \(\operatorname{mex}(T)\)
求所有非空子区间的权值和。对 \(998244353\) 取模。
\(n\le 10^6,1\le a_i \le 10^9\)

额说明一下,\(S'\) 是可以取空集的。

首先,这个“子集和构成的集合”的 mex 怎么算。那显然就是按值域从小到大扫,找到一个 \(x\),使得小于 \(x\) 的数的和加一,比 \(x\) 还要小,那么这个 mex 就是那个和加一。正确性显然。
考虑一个 \(O(n^2\log n)\) 的暴力,即枚举右端点,枚举左端点,用一个什么 set 维护一下啥的。

然后在 set 扫的过程中,可能需要讨论:我在上一次处理的区间,开头加进来一个 \(x\),若 \(x\) 大于上一次的 mex,则 mex 不变;否则扫描 set 里面的迭代器,暴力更新 mex。
然后我 mex 发生变化,还分为两种情况。一种是,我没有触发连锁反应;另一种是,我触发了连锁反应,会顶到更大的 \(a\)

那么关键的就是,我连锁反应只会有 \(O(\log V)\) 轮。

额正解就是把这个东西换成扫描线,并且动态维护每个左端点的答案。
首先就是,右端点 append 一个数,mex 是不降的。
额一个很牛逼的东西叫做倍增分块。

想到这个是因为,上面那个东西有一点,值域翻倍的味道?好像有点牵强。

那这一题,倍增分块有非常优良的性质。
这里维护 mex 在 \([2^k, 2^{k+1})\) 的值。
注意到,左端点递增的 mex 是不升的,所以值域段在原序列上,也是一些区间。
考虑我怎么判定一个 \([l, r]\) 的 mex 在 \([2^k, 2^{k+1})\) 中?
我查找所有 \([l, r]\)\(a_i\) 中,小于 \(2^k\) 的数的和。这个和加一,如果在 \([2^k, 2^{k+1})\) 中,且比 \(b\) 小,且这个 \(k\) 是最小的满足条件的 \(k\),那么判定成功。
这个 \(b\) 是,\([l, r]\) 中,\(a_i\) 在块值域中的最小值。(意思是不会连锁反应)
进一步地,此时我的 mex 就是这个和加一。

这有点代码细节的意思了。
反正大概就是,我维护每个值域段,然后考虑我变块只会发生 \(O(\log V)\) 轮,然后每次 append 只用管连锁反应的。
讲不清楚了。

int ans = 0, s = 0;
upw(i, 0, 59) L[i] = 1, R[i] = 0;
upw(i, 1, n) {
	R[0] ++, s ++;
	int lg = __lg(a[i]);
	upw(j, lg, 59) {
		while(L[j] <= R[j]) {
			int t = (j == 0 ? 0 : sum[j - 1][i] - sum[j - 1][L[j] - 1]) + 1;
			int v = min(qmin(j, L[j], i), 1ll << (j + 1));
			if(v <= t) {
				int mex = (j == 0 ? 0 : sum[j - 1][i - 1] - sum[j - 1][L[j] - 1]) + 1;
				s -= mex;
				int newmex;
				s += (newmex = sum[j][i - 1] - sum[j][L[j] - 1] + 1);
				++L[j], ++R[j + 1];
			}
			else break;
		}
	}
	s += R[lg + 1] * a[i];
	s %= 998244353;
	ans = (ans + s) % 998244353;
}
cout << ans << '\n';

然后这个 qmin 是可以单调队列的,复杂度一个老哥。

追忆

怎么这题都一岁了我还没写。

那么看到这题第一反应是啥。不弱于传递闭包。那先把传递闭包搞出来。
然后考虑我可以先求出来,每个询问 \(x\) 出发能到达的所有 \(a\) 值在 \([l,r]\) 内的点集 \(S\),再考虑对着这个点集求出 \(b\) 中的最大值。

首先求 \(S\) 是简单的。随便 bitset。具体地,考虑值域分块,\(B\) 个一块,每块记录 \(a\) 值在这个区间的下标集合,散块暴力。那么不注意点的话每次询问要 \(O(\frac nB)\) 次 bitset 操作,寄了。那么改为维护前缀和,询问差一下就好。修改 \(a\) 也只会进行 \(O(\frac nB)\) 次操作。
时空瓶颈不在这里,\(B\) 取个根号就差不多了。

然后考虑怎么对着 \(S\)\(b\) 的 max。
那么一个朴素的想法是,仍然值域分块,但是这里会多一个二分,二分到最短的块后缀,使得 bitset 并起来,和 \(S\) 有交。多一个 \(O(\log w)\),不可接受。

那考虑更精妙的做法。
仍然取一个后缀并,即求出 \(T_i=\{p|b_p\ge iB\}\)
然后我们想定位最短的后缀块 \(pos\),使得和 \(S\) 有交。进行如下算法:

  • 初始令 \(i=1,pos=0\)
  • 每次尝试给 \(pos\) 加一,看 \(T_{pos+1}\)\(S\)\(i\) 个 ull 位有没有交。有交就扩展。
  • \(i\gets i+1\)

牛完了。正确性显然,然后会扩展 \(O(\frac nB)\) 次。定位到这个目标值域块后暴力扫一遍就行了。

总复杂度 \(O(\frac {n(n+q)}w + q\sqrt n)\)

posted @ 2026-01-08 11:37  Water_M  阅读(15)  评论(2)    收藏  举报