数据结构tricks

综合性 tricks

时间倒流

如果只有删除操作但是不好删除,考虑让时间倒流,从最后的状态不断加入。其余同理。

适用于很多想法,如平衡树维护凸包区间染色图论删除/加入点等等。

插入标记回收

解决的问题是:\(q\) 次询问 \(x\) 依次经过 \(r−l+1\) 种操作的结果。

\(i\) 种操作是 \(x←F_i(x)\) 这样。可以离线。

算法流程如下:

  • 插入数据。在 \(l\) 时间开始前把 \(x\) 加入集合 \(S\) 中。
  • 标记。在 \(i\) 时间执行 \(S←F_i(S)\)
  • 回收数据。在 \(r+1\) 时间开始前把 \(l\) 丢进去的数据从 \(S\) 中回收。

这里的 \(S\) 采用数据结构维护。比如使用平衡树

如果是强制在线也能做。先对操作时间线分块,对于每一块,维护值域内的所有值在经过整个块的操作后会变成什么。如果值域太大怎么办?有些时候,超过一定值域的操作会是固定的,只需二分出每个数进入该值域是位于哪一次操作即可。

区间维护类数据结构的异同

算法 适用范围 支持操作 时间复杂度
线段树 可差分不可重(如加法)可重复不可差分(如最值) 在线区间修改与区间查询 预处理:\(O(n\log n)\\\)修改:\(O(\log n)\\\)查询:\(O(\log n)\\\)常数较大
4-Russia/分块ST表 可重复不可差分(如最值) 离线区间查询 预处理:\(O(n)\\\) 查询:\(O(1)\\\)
快速ST表 可重复不可差分(如最值) 离线区间查询 处理:\(O(n\log n)\\\) 查询:\(O(1)\\\)
倍增ST表 可差分不可重(如加法)可重复不可差分(如最值) 离线区间查询 预处理:\(O(n\log n)\\\) 查询:\(O(\log n)\\\)
树状数组 可差分不可重(如加法) 在线单点修改与区间查询(或区间修改与单点查询) 预处理:\(O(n)\\\) 修改:\(O(\log n)\\\) 查询:\(O(\log n)\\\)
双倍树状数组 可差分不可重(如加法) 在线区间修改和区间查询 预处理:\(O(n)\\\) 修改:\(O(\log n)\\\) 查询:\(O(\log n)\\\) 常数是树状数组的2倍
差分+前缀和 可差分不可重(如加法) 离线区间修改和单点查询(或者单点修改和区间查询) 预处理:\(O(n)\\\) 查询:\(O(1)\\\)

线性数据结构

双指针

用途:\(O(nk)\) 维护整个序列中:(1)所有最长的合法子区间的长度,或者 (2)特定长度子区间的价值。其中 \(O(k)\) 是边界移动一次的时间复杂度。

限制:必须 \(O(k)\) 维护边界移动(一般来说是常数级的复杂度)。对于情况(1),区间的合法性必须关于长度具有单调性(即序列越短越合法)。对于情况(2),无限制。

做法:问题(1)不断向右移动右端点,然后移动左端点使得答案合法,记录当前区间长度。问题(2),不断移动左右端点并统计答案。

单调栈/单调队列

用途:\(O(n)\) 计算序列中每个位置 前/后 固定距离内第一个 大于/小于 它的数字。(也可以不限制距离)

做法:用一个双端队列,队尾维护单调,队首是答案。每次遍历到下一个位置时,把这个位置塞进队尾,一路把那些比它更劣的元素扔出来。

只用跑一遍单调栈就能得到前后比该位置小的第一个位置: 入栈前的栈顶就是前面比它小的第一个位置,把它弹出的数就是后面比它小的第一个位置。

如果一个选手比你年龄小还比你强,那么你就可以退役了。

——单调队列

线段树

线段树二分

线段树二分是指在线段树上查找某一特定位置(如第一个前缀和 \(\ge k\) 的数字)

全局的线段树二分是简单的。直接判断要找的点在左子树还是右子树即可。

区间的线段树二分怎么做呢? 对于每个递归:如果当前区间长度为 1,是解就返回,否则返回无效值如 \(-1\),表示无效解;如果当前区间被包含在了查询的区间中,就直接判断进入哪个儿子;如果当前区间不完全被查询区间包含,但是与其相交(这是肯定的),像正常的 query 一样分割(查询区间与儿子有交就进入)。

动态开点

需要开很多棵线段树(或者线段树值域很大)的时候,使用动态开点节省空间和时间。请注意其常数比一般线段树还要大。

维护每棵树的 ,每个节点的 左、右儿子。修改时,访问到尚不存在的节点就创建一个新的(并赋初始值)。查询时,访问到尚不存在的节点就返回空值。

可持久化线段树(和主席树)

线段树合并

线段树合并用于解决:要开很多线段树储存信息,而且这些线段树之间存在递推关系,修改数量是可线性做的,这一类问题。常见于树形 DP。

简单来说,合并两棵树就是不断递归向下:如果该节点在两棵树中都存在,合并两个点;如果该节点在两棵树中都不存在,返回;如果该节点只在一棵树中存在,取这个点。

合并时,可以改变求和顺序算出前缀和,类似cdq,详见P6773 [NOI2020] 命运

复杂度证明:两个节点都存在时,节点数减少 \(1\),时间、空间复杂度增加 \(O(1)\);其余情况不增加复杂度。修改时,时间空间复杂度增加 \(O(\log n)\),节点数增加 \(O(\log n)\)。综上,复杂度为 \(O(m\log n)\) 量级。

摊子树为线段

如果要进行子树全体的操作,考虑按照dfn把节点排列在线段上,并记录x点子树中最大的dfn(通常是返回时的dfc)为 \(lst_x\) ,这样对x点子树的操作就等价于对线段 \([dfn_x,lst_x]\) 操作。

如果不行,考虑树上差分或是树链剖分。

区间二分剪枝

暴力区间二分不会减少复杂度,但是如果剪枝就不一样了。如果能够进行剪枝保证每层只有 \(n\) 个区间往下递归,那么复杂度就是 \((n\log m)\) 。这是类似懒标记线段树的复杂度分析。例如CF1111C Creative Snap

吉司机线段树

线段树区间最值操作、查询。

例子:区间cmax(和k取最值),区间加,区间查询sum,区间查询max。

维护区间最大和次大的两个数,保证他们不同。cmax时,如果 \(k\ge first\) 则不动;如果 \(second\le k<first\) 则打上一个tag(sum的修改只会对值为 \(first\) 的数发生,因此可以维护);如果 \(k<second\) 则暴力递归进去修改。

势能分析复杂度:设势能为整棵线段树所有节点取值的种类数,那么每一次cmax的暴力会导致一个点的势能至少 \(-1\),而每一次区间加会导致整棵树的势能至多 \(+\log n\),而初始的势能至多是 \(n\),因此最后的总的复杂度是 \(O(m\log n)\)

李超线段树

添加线段,求某一 \(x\) 所有线段 \(y\) 的最大值。可用于斜率优化(见DPtricks)

OI-wiki挺好的,可看。


以下部分是一些经典的例题

查询区间内最大的子段平均值(子段长度 \(\ge m\)

问题

一、区间修改(加法、乘法等)

二、查询区间 \([L,R]\) 中,所有 \(L\le l,r\le R\) 而且 \(r-l+1\ge m\) 的区间的平均值( \(\frac{\sum_{i=l}^{r}a_{i}}{r-l+1}\) )的最大值。

解决方法

\(f(A)\) 为区间 \(A\) 的子段平均值的最大值。

使用记号 \(A+B\) 表示相邻区间 \(A,B\) 的并。

那么有性质 \(f(A+B)\le max(f(A),f(B))\) 。可以用反证法证明。

由此可见,答案区间的长度一定越小越好。如果没有 \(len \ge k\) 的限制,我们就取区间最大值即可。如果有 \(len \ge k\) 的限制,我们取 \(len\in \{s\in\mathbb{N}_{+}|k\le s\le 2k-1\}\) 的区间一定最优。

由此我们可以维护 \(k\) 个线段树,维护每个点为起点长度为 \([k,2k-1]\) 的子段的平均值,在查询max即可。

修改/查询时间复杂度:\(O(k\log n)\)

修改/查询空间复杂度:\(O(kn\log n)\)

选择至多 \(k\) 个不相交子段,求和的最大值

\(O(n\log n)\) 反悔贪心(线段树)

根据费用流模型(虽然我不会),选取一个区间后,将这个区间取相反数,这样下一次选取就相当于取消这些位置的选取。之后每次选取和最大的区间即可。使用线段树维护即可。

\(O(n)\) 反悔贪心(不是线段树)

https://www.luogu.com.cn/article/ttawg1d8

树状数组

技巧

\(O(n)\) 建树:t[i]=a[i]-a[i-lowbit(i)]

\(O(\log n)\) 二分:如果我们在位置 \(p\),我们不断往前跳,跳的步长依次递减,\(2^{30},\ 2^{29},\ \dots,\ 2^1,\ 2^0\)。如何判断 \(p+d\) 是否符合要求呢?我们直接累加 \(t[p+d]\) 即可。根据树状数组的性质 \(t[p+d]\) 这个位置刚好存储了 \([p+1,p+d]\) 这个区间内的所有信息,因此这样直接累加的做法是正确的。

理论上,树状数组可以维护最大值,但是是 \(O(nlog^2n)\) 的,所以为什么不用线段树呢

后缀和树状数组

把qry和chg的循环部分交换:

void add(int x,int num){for(int i=x;i;i-=lowbit(i))c[i]=min(c[i],num);}
int qry(int x){int res=inf;for(int i=x;i<=n;i+=lowbit(i))res=min(c[i],res);return res;}

二维树状数组

将树状数组中的 \(+\) 替换为 \(add\) 操作即可。

区修区查/矩修矩查

使用树状数组维护差分数组的前缀和。

差分数组d——原数组a——前缀和数组s

上述三者构成转化关系,相邻两个可以通过前缀和和差分互相转化。推式子:\(s_n=\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{i}d_j=\sum\limits_{j=1}^{n}\sum\limits_{i=j}^{n}d_j=\sum\limits_{i=1}^{n}(n-i+1)d_i=(n+1)\sum\limits_{i=1}^nd_i+\sum\limits_{i=1}^nid_i\) 。然后就可以用树状数组维护了。

时间和空间常数均比线段树小一倍。

矩阵修改矩阵查询同理,而且比线段树好写4倍。

逆序对

逆序对的定义是:一个序列 \(a\) 中的一个数对 \((i,j)\) ,使得 \(i<j,a_i>a_j\)

逆序对还与很多问题相关:

邻项交换最小次数

一个序列中的逆序对数目就是邻项交换的最小次数。

证明:一次邻项交换会使得被交换的两个元素之间的逆序对消失,而其他的逆序对不受影响。因此,逆序对数目 \(=\) 邻项交换的最小次数。

\(K\) 维数点和扫描线

二位数点是指给定一些点,求一个二维前缀范围内的有多少点。

一个在线 \(k\) 维数点问题等价于一个离线 \(k+1\) 维数点问题。事实上,这是时间维度与一个空间维度之间的互相转化。使用这种思想,我们可以把在线和离线问题互相转化。这种思想的一个例子是扫描线。

扫描线

使用线段树(有时也可以用树状数组)维护一条线上的值,然后把这条线扫过平面,并把询问按照时间方向离线到时间轴上。

CDQ分治

简介

CDQ 分治是一种思想而不是具体的算法,与 动态规划 类似。目前这个思想的拓展十分广泛,依原理与写法的不同,大致分为三类:

  • 解决和点对有关的问题。
  • 1D 动态规划的优化与转移。
  • 通过 CDQ 分治,将一些动态问题转化为静态问题。

CDQ 分治的思想最早由 IOI2008 金牌得主陈丹琦在高中时整理并总结,它也因此得名。

基本思想

简单来说,cdq分治可以用 \(O(\log n)\) 级复杂度解决数点统计类问题的一个维度。

其基本思想是,每次不断二分,然后进行三个操作。

  • 递归进入左边的区间。
  • 处理跨越中间的边界,横跨左右的贡献。
  • 递归进入右边的区间。

如何处理第二部就是核心难点,也是 CDQ分治 用途如此广泛的关键。对于三维数点问题来说,第二部是用线段树维护左侧的修改对右侧的查询操作产生了多少贡献。

Trick

正如简介所提到,可以把一些 \(k\) 维的动态问题加入一个时间轴转化为 \(k+1\) 维的静态问题。比如持续一段时间一定矩形范围的操作在这个时空图上看来就是一个立方体。

单点修改区间求mex

https://www.cnblogs.com/QedDust/p/17856330.html
https://www.cnblogs.com/DM11/p/17020308.html

平衡树

FHQ-Treap 值域带交合并

先根据 \(rnd\) 决定 \(x,y\) 谁当根。设 \(x\) 当根。把 \(y\) 按照 \(val_x\) 分裂,把分裂出的两半分别与 \(x\) 的左右儿子合并即可。

wait!这样做会有一个致命的问题! 每次合并时,若 \(val_x=val_y\)\(y\) 永远在 \(x\) 的右儿子一侧,因此会导致平衡树深度失衡。我们只需要改变我们的步骤“把 \(y\) 按照 \(val_x\) 分裂”,改为 “把 \(y\) 按照 \(val_x-rand(0,1)\) 分裂”,即可做到随机分配左右儿子。或者也可以像这样

image

当然,这样做其实是偷懒的做法。如果要追求更好的复杂度,应当把所有权值相同的点合并到一起。但是这样的话我不是很会了。

例题。lmy太强了,写的程序又比我快 \(10\) 倍。

posted @ 2025-02-20 21:09  Luke_li  阅读(5)  评论(0)    收藏  举报