这篇博客主要是扒 \(wzh\) 大佬的课件的,对于一些做题的思路有个人的理解。
基础数据结构
一般数据结构
都进 \(\text{OI}\) 很久了,这些基础数据结构都应该知道:
- 栈:后进先出的存储结构。
- 队列:先进先出的存储结构。
- 堆:结构是完全二叉树,用于支持插入、删除和求最值的基本操作。完成各操作主要通过每个点的权值一定为子树的最值这个性质展开。
- 左偏树(可并堆):结构是二叉树,是可以合并的堆。因为插入和删除可以用合并实现因而基本操作只有合并。对每个节点维护往右走到叶子的步数 \(dis\) 。并保证 \(dis(left(x))\le dis(right(x))+1\) , \(dis(right(x))\le dis(left(x))\) 。用归纳可以得出树高级别为 \(\mathcal O(\logn)\) 。基于该性质可以保证复杂度,便在合并操作中维护该性质即可。
进阶数据结构
树状数组
对一个序列进行信息维护,支持单点修改、区间查询,要求信息具有可合并性。
一般只对有可减性信息作维护(例如和、异或和)而不对没有可减性信息作维护(例如最值),维护没有可减性信息将增加树状数组的复杂度。
以维护区间和为例,树状数组将记数组 \(C_i=Sum(i-lowbit(i)+1,i)\) 。基于 \(lowbit\) 的性质在维护时不论修改还是询问都只需要访问 \(\mathcal O(\logn)\) 个位置。
而 \(lowbit\) 可这样实现
inline int lowbit(const int i){return i&(-i);}
线段树
一棵二叉树结构,对一个序列进行信息维护,支持单点修改,并在修改标记具有可合并性时支持区间修改,亦支持区间查询,要求信息具有可合并性。
每个节点代表了一个区间,一个区间的左右儿子恰是该区间对半分的两边,因而该结构树高为 \(\mathcal O(\logn)\) 。区间的修改和查询可以被拆分到 \(\mathcal O(\logn)\) 个线段树节点代表的区间上。
编号方法如果采取左儿子的编号是乘 \(2\),右儿子的编号是乘 \(2\) 加 \(1\),则最坏情况下编号会到 \(4n\),需要 \(4\) 倍空间。线段树实际只有 \(2n\) 个点,而动态开点虽然只有 \(2n\) 个编号,但由于需要多维护左儿子和右儿子指针需要 \(6\) 倍空间 十分高级的逆向优化
在有些毒瘤空间严苛的题目里,采取如下函数的编号方法,可以使线段树中序遍历从 \(2\) 开始的一段连续,即点的编号从 \(2-2n+1\),只需要 \(2\) 倍空间。
inline int getid(const int l,const int r){return l+r|l!=r;}
线段树的区间修改通过懒标记实现。
我们并非将所有需要修改的区间直接修改,而是发现一次区间修改可以分解为 \(\mathcal O(\log n)\) 个线段树的子树修改,在子树的根上用懒标记表示子树需要修改,并将根节点的值修改以方便到根路径的信息更新。
有了懒标记,只要在任何线段树自上而下的过程中将标记推下即可。
由于一个区间可能被修改多次,而将标记堆成队列复杂度很高,因而要求标记具备可合并性(如 \(+2\) 和 \(+3\) 可以合并成 \(+5\))
在涉及多种修改类型时,应该设定好它们的执行顺序,如同时有区间加和区间乘,可以定义先乘再加(一个区间的标记是 \(\times 5\) 和 \(+3\) 意味着一个 \(x\) 应该修改成 \(5x+3\))
例题
- 「BZOJ-3821」玄学
给一个长度为 \(n\) 的序列 \(a\) 以及一个长度为 \(m\) 的修改操作序列,每一项是一个形如将 \([l,r]\) 内的每一个 \(a_i\) 修改为 \(a\times a_i+b\) 的修改操作。
你需要回答q个询问,每个询问形如按从左到右的顺序依次执行修改操作序列区间[l,r]的修改操作后序列 \(a\) 中 \(a_k\) 的值是多少。询问互相独立。
\(n,m+q≤500000\)。
这里介绍一种离线做法(还不足以 \(A\) 掉这道题,此题强制在线)
可以考虑扫描线。把修改操作两端点以及询问操作的位置放在一起排序然后扫描。
修改操作是一次函数,其的叠加仍是一次函数(下面会具体讲如何合并)。
以修改操作序列建线段树,区间维护依次执行完对应所有修改操作后一次函数的系数。 该信息是可合并的。
即若左儿子是 \(ax+b\) 右儿子是 \(cx+d\) 将会合并为 \(c(ax+b)+d=acx+(bc+d)\) 。
在同一个位置,按修改操作左端点,询问,修改操作右端点的优先级访问。
如果扫到修改操作左端点,加入到线段树中,扫到右端点即在线段树中删除。
扫的询问操作直接在线段树里询问出一次函数,将该一次函数作用于序列原来的值即可。
时间复杂度 \(\mathcal O(n\logn)\) 。
- 「BZOJ十连测」线段树(节选)
有一个长度为 \(n\) 的序列与 \(m\) 个修改操作,每个修改操作是将序列 \([l,r]\) 的元素都修改为这个区间的最大值。
现有 \(q\) 个操作,要么是修改序列的一个元素,要么是询问执行 \([l,r]\) 的修改操作后,第 \(k\) 个元素是多少。询问之间独立,而修改会造成影响。
\(n,m,q≤100000\)。
我们容易发现,每一个位置都可以被表示成一段区间的最大值。
我们枚举修改操作序列右端点r来离线做,把所有询问操作挂在其对应右端点上。
例如位置 \(k\),找到当前操作前最后一个覆盖其的操作,然后继续找最后一个覆盖该区间左端点的操作,最后一个覆盖该区间右端点的操作,最后一个覆盖最后一个覆盖该区间左端点的操作区间的左端点……
也就是,我们需要一直往左找,一直往右找,找到最左的 \(ll\) 与最右的 \(rr\),使得 \(k\) 这个位置可以表示成 \([ll,rr]\) 的最大值。
把每个操作当做一个结点,我们维护两颗树:一棵叫左树一棵叫右树,左树中,一个结点的父亲所对应区间是在该结点之前的最后一个可以覆盖其左端点的区间,右树同理维护往右。倍增一下,便可以快速找到 \(ll\) 与 \(rr\)。
如何得知最晚覆盖其的区间?可以用线段树,每加入一个区间就区间赋值。
然后,顺序扫 \(q\) 个操作,用线段树维护 \(a\),遇到修改就修改,遇到询问就询问对应区间最大值。
线段树的合并
合并两颗线段树的过程非常简单。
如果两颗有其中一颗为空,即可返回另一颗。否则递归合并左右子树,用其中一个作根,并合并信息。
然而,单次线段树合并最坏的复杂度显然是 \(\mathcal O(n)\) 的。
一般常用的是将原本 \(n\) 颗大小为 \(1\) 的线段树最后合并成一颗,复杂度是 \(\mathcal O(n\log n)\) 的。
设势函数为线段树的节点数,则由初始可得势函数不超过 \(n\log n\)。每次线段树的合并复杂度若为 \(k\),则意味着合并了 \(k\) 个节点,将会使得势函数减少 \(k\) 。因而通过势能分析可得这样做的复杂度是 \(\mathcal O(n\log n)\) 的。
平衡树
平衡树是复杂度有保证的二叉排序树。
二叉排序树:中序遍历有序的二叉树,显然形态不唯一。
常用平衡树
常用平衡树为 Splay
,Treap
和替罪羊树。
Splay
:依靠伸展操作(splay
操作)完成平衡。可以支持分裂与合并。均摊复杂度 \(\mathcal O(\logn)\)。Treap
:给每个节点随机优先级 \(fix\),并保证 \(fix\) 形成堆的结构,来完成平衡。亦分为旋转版本(插入为基本操作)和非旋转版本(合并为基本操作),都能支持分裂与合并,期望复杂度 \(\mathcal O(\log n)\)。- 替罪羊树:对设定常数 \(a(0.5<a<1)\),保证每个节点的儿子子树大小不超过其子树大小的 \(a\) 倍完成平衡。每次操作后将最高不满足要求的子树重构。均摊复杂度 \(\mathcal O(\log n)\)。
Splay
和 Treap
可以进行启发式合并。在之前的合并操作中,其中一颗的排序将严格小于另一颗,如果没有这样的保证,则应该进行启发式合并。
将大小较小的那颗平衡树按中序遍历将节点插入进另一颗中即可。
单次合并的复杂度无法保证,但根据定理可以保证将 \(n\) 颗大小为 \(1\) 的最终合并为一颗复杂度为 \(\mathcal O(n\log n)\)。
重量平衡树
重量平衡树:单次基本操作所影响的最大子树的大小的最坏/期望/均摊是 \(\mathcal O(\log n)\) 的平衡树称为重量平衡树。
替罪羊树由于每次是暴力重构子树,因而其是重量平衡的。
Treap
亦可证明是一种重量平衡树。
假设新插入了一个节点,旋转到了祖先 \(k\) 这个位置。那么影响的大小就是 \(size[k]\)。
又因为 Treap
的定义,新插入点的 \(fix\) 在 \(k\) 的子树中一定最小,这个概率应当是 \(\frac{1}{size[k]}\) 的,那么对期望的贡献是 \(1\) 。
Treap
期望树高为 \(\log n\),因此每次期望影响大小是 \(\log n\)。
而从 Treap
中删除一个节点,直接暴力重构该节点的子树即可。因为期望树高 \(\log n\),祖先后代的关系数期望为 \(n\log n\),也即子树和期望为 \(n\log n\),子树大小的期望为 \(\log n\)。
那么可看出 Treap
单次基本操作所影响子树大小期望为 \(\log n\),符合重量平衡树的定义。
例题
- 阿凡达(出处未知)
维护一个长度为 \(n\) 的序列 \(A\),初始全为 \(0\)。
有 \(q\) 次操作,共两种:
- 把 \([l,r]\) 区间每个 \(A[i]\) 修改为 \((i-l+1)∗X \mod Y\);
- 询问区间 \([l,r]\) 的和;
\(n≤10^9,q≤50000\)。
考虑虑将一次修改的区间看作一个颜色段。
那么整个序列无时无刻都由若干颜色段组成,且颜色段数量为 \(\mathcal O(q)\)。
用平衡树维护颜色段,每个点上存储一段的和。
修改分情况讨论对原先一些颜色段分裂或覆盖。
剩下问题是我们如何计算一个颜色段的和。
注意到 \((i-l+1)∗X \mod Y=(i-l+1)\times X-Y\times ⌊\frac{(i-l+1)∗X}{Y}⌋\)
前者很好求和,后者可以用经典的类欧几里得算法。总复杂度一个 \(log\) 。
来源:https://www.cnblogs.com/Arextre/p/12215750.html