简单数据结构:线段树,树状数组
起因是周五终于不用考试了。
线段树
线段树是一种用来维护一个长度为\(n\)的序列的数据结构,基本的,其可以做到\(O(log)\)修改或者查询某些序列关键信息。
具体的,其为一个包含\(2n-1\)个点的满二叉树结构,每个节点都代表一个区间,\(n\)个叶子结点代表\(n\)个原序列元素,非叶结点代表其两个儿子的区间并。
e.g.1
维护一个序列,\(m\)次查询区间和,最值,乘积,最大子段和。
\(sum(l,r)=sum(l,mid)+sum(mid+1,r)\)
\(max(l,r)=max(max(l,mid),max(mid+1,r))\)
\(mul(l,r)=mul(l,mid) * mul(mid+1,r)\)
令\(pre(l,r),suf(l,r),mx(l,r)\)表示前缀和,后缀和,子段和。
则
\(pre(l,r)=max(pre(l,mid),sum(l,mid)+pre(mid+1,r)\)
\(suf(l,r)=max(suf(mid+1,r),sum(mid+1,r)+suf()l,mid)\)
\(mx(l,r)=max(mx(l,mid),mx(mid+1,r),suf(l,mid)+pre(mid+1,r)\)
对于修改,最简单的情形是单点修改,我们直接暴力对对应的叶子结点以及其祖先的一条链更新即可,复杂度\(logn\)
对于区间修改,我们考虑对修改的区间进行拆分,具体的,表现为对每个节点额外维护懒标记,将修改的信息先存在懒标记中,之后只要经过了该点,我们就将该点的懒标记进行下放,即\((l,r)->(l,mid),(mid+1,r)\)
另外,当存在多个懒标记时,其下放有先后顺序,如 P3373
对于该题,我们显然要维护一个\(lztag\)的二元组\((mul,add)\),但当前点的乘法表及会对儿子的加法标记有倍增效应,所以我们在下放标记时,应先乘\(mul\)再加\(add\).
这里给出ix35爷举的常见标记下放。
权值线段树
如果我们令线段树下标表示一个数,每个节点就等价于一段值域,我们称这样的线段树为值域线段树。
e.g.2
给定一个长为\(n\)的数列,求有多少子段和在\([l,r]\)内、
\(n\leq 10^5,l\leq r\leq 10^9\)
我们对序列求前缀和,问题转化为有多少\(l\leq s[r]-s[l-1]\leq r\)
我们对\(s\)建权值线段树,每次加入\(s_r\)前查询满足条件的\(s_{l-1}\)个数,当然此题\(l,r\)的范围过大,但\(n\)范围较小,所以需要动态开点。
动态开点
实质上就是不把满二叉树在建树的过程中直接建出来,而是根据输入量即加即建。
*主席树
即可持久化权值线段树,我们在修改的时候,在原线段树旁额外维护一条链,表示其历史版本即可。
e.g.3
给定一个区间,求区间\([l,r]\)第\(k\)大数。
我们考虑对于每次插入新建一个版本,那么\(tr[rt[i]].val\)实质上保存的是第\(i\)个版本总共插入了多少数值,我们求一个区间的\(k\)大,令该点左儿子有\(cnt\)个数,当\(k\leq cnt\)时,我们直接查左儿子第\(k\)小,否则查右儿子\(cnt-k\)小即可。
如果需要区间修改,则需要用到标记永久化,这里不展开讨论。
下面是一些线段树杂项。
zkw线段树
即非递归式线段树,考虑自底向上建树,具体见早期洛谷日报(博主名脑叶好评)
线段树二分
考虑这样一个问题:维护一个长度为\(n\)的\(0,1\)序列,支持单点修改,查询左数第\(k\)个\(1\)的位置。
如果我们用线段树维护这个序列,有一个暴力的想法即是在每次询问时二分答案\(pos\),求出\(
pos\)处的前缀和与\(k\)比较。
这样的复杂度是\(log^2\)的
。注意到线段树本身就是一个分治的结构,我们考虑直接在线段树上进行二分。
具体的,令\(Solve(l,r,k)\)表示在区间\([l,r]\)中寻找第\(k\)个\(1\),令\(sum\)为\([l,r]\)的区间和,若\(sum\geq k\),则说明第\(k\)个\(1\)在区间\([l,mid]\)处,否则在\([mid+1,r]\)处,递归即可做到\(O(log)\)
由于是基础所以没有线段树合并一类的东西
树状数组
取出\([1,2^n]\)标准线段树的所有左儿子和根,构成一个森林结构,就是树状数组。\(--zkw\)
树状数组比线段树的有点在于时间与空间常数均要小一些,且十分好写,但问题在于它只能维护前缀的信息。
具体的,我们设\(\operatorname {lowbit}(x)\)表示\(x\)在二进制下最低位\(1\)的位置所代表的的权值,考虑一个\([1,n]\)的树状数组,节点\(i\)维护的为区间\([i-\operatorname{lowbit}(i)+1,i]\)的信息,在查询\([1,x]\)时,我们只需令\(i=x\),每次令\(i:=i-\operatorname{lowbit}(i)\)即可。
树状数组大多数时候能被线段树替代,除非有出题人恶意卡常,或者你打不来线段树(?)
大概就这些。