王队线段树
又名:兔队线段树,楼房重建线段树,单调栈维护前缀最值/单调栈
P4198 - 楼房重建
https://www.luogu.com.cn/problem/P4198
显然我们转化成斜率之后,要求的就是前缀最大值个数。
我们设 \(tre[l,r]\) 代表考虑 \([l,r]\) 的所有点,\([mid+1,r]\) 的前缀最大值个数。
设 \(\max[l,r]\) 为 \([l,r]\) 间的最大值。
再设 \(c([l,r],x)\) 为 \([l,r]\) 之内只考虑 \(\ge x\) 的点,的前缀最大值个数。
显然 \(tre[l,r]=c([mid+1,r],\max[l,mid])\)。
这个 \(c\) 就像一个 query
函数,怎么样求出呢。
分情况讨论:
- 若 \(\max[lson]< x\) 则 \(c([l,r],x)=c([mid+1,r],x)\);
- 否则 \(c([l,r],x)=tre[l,r]+c([l,mid],x)\)。
答案就是 \(c([1,n],0)\) 啦!
这个显然是 \(O(\log^2)\) 的。
P4425 - [HNOI/AHOI2018]转盘
https://www.luogu.com.cn/problem/P4425
待填
诚哥线段树
又名:线段树维护历史信息。
当然,这种问题用分块就很好解决,因为不用 pushdown。
https://www.cnblogs.com/LightningUZ/p/14726808.html
区间历史最大值
操作 1:区间加上一个数字 \(x\)。
操作 2:查询区间历史最大值。
我们维护每个节点的:
当前的区间最大值 mxa
,历史最大值 hmxa
,lazy 的值 laz
,lazy 的历史最大值 hlaz
。
画一下操作轴:
+x+x+x+Xmax+x +x+x+Xmax+x+x
我的操作 我的父亲的操作
Xmax为历史最大值的位置。
我们现在要将父亲的操作 pushdown 到我的节点上。
那么
hmxa[o]=max(hmxa[o],mxa[o]+hlaz[fa]);
hlaz[o]=max(hlaz[o],laz[o]+hlaz[fa]);
mxa[o]+=mxa[fa];
laz[o]+=laz[fa];
即可。注意代码顺序不能变,否则会用自己的信息来更新。
P4314 - CPU 监控
https://www.luogu.com.cn/problem/P4314
也就是区间加,区间覆盖,区间历史最大值。
发现第一次区间覆盖后的操作,无论是加还是覆盖,都可以视为区间覆盖。因为第一次区间覆盖后全部数字都相同了,设为 \(x\),那么加 \(c\) 就相当于覆盖为 \(x+c\)。(也就是直接将赋值标记 +c 即可)。
所以一个点对应的操作序列必定是一段区间加,一段区间覆盖。
于是分类讨论一下,是父亲的加操作会更新我的历史最大值,还是父亲的覆盖操作会更新我的历史最大值即可。
比如我的操作是这样的:+x+x+x+x...=x=x=x=x 设我的历史最大值为 Imax
1. 父亲加操作 来更新 [+x+x+x...=x=x=x] [+x+Xmax+x...=x=x=x]
那就和上文一样用 Xmax 加上我的和
2. 父亲覆盖操作 来更新 [+x+x+x...=x=x=x] [+x+x+x...=x=Xmax=x]
那么就直接 chkmax 我的值
所以我们也要记录 fz,hfz
代表赋值的 laz 以及赋值的 laz 的历史最大值。
区间历史版本和
https://www.cnblogs.com/guangheli/p/13274276.html
操作 1:区间加上一个数字 \(x\)。
操作 2:查询区间历史版本和。
先想想普通的线段树:维护 \(sum\) 代表区间和,维护 \(laz\) 代表懒标记。
现在我们多维护:
\(hsum\) 代表历史区间和。
\(tag\) 代表距离上一次被更新时间,有多少时间了;或者说,使得 \(hsum:=hsum+tag\times sum\) 的 \(tag\)。
但是我们还需要维护一个东西,叫 \(dlt\)。比如我们两次操作都是落在一个区间,一次 +5,一次+7,那么我们会将 \(laz=12,tag=2\) 给 pushdown,这显然是不对的,我们需要减去 \(dlt\) 以抵消算多的部分。计算 \(dlt\) 的方式为,如果我个区间 +val,区间的 \(tag\) 为 \(x\)(此时 \(tag\) 还没被更新)那么 \(dlt+=x*val\)。比如 +5 和 +7,那么先有 dlt+=5X0,然后有 dlt+=7X1,那么 pushdown 的时候,就会是 \(hsum:=hsum+tag*sum=hsum+3*12\),减去 \(dlt\) 后就是 \(3*12-dlt*tag=3*12-7*3=15\) 演算无误,完美无瑕。
下传标记顺序为 \(laz,dlt,tag\)。
伪代码如下:
并没有经过调试,没有确保正确性。
struct mist{int sum,hsum,laz,tag,dlt;}tre[];
void pddlt(int o,int len,int val){
tre[o].dlt+=val;
}
void pdlaz(int o,int len,int val){
tre[o].dlt-=val*tre[o].tag;
tre[o].laz+=val;
tre[o].sum+=val*len;
}
void pdtag(int o,int val){
tre[o].tag+=val;
tre[o].hsum+=val*tre[o].sum;
}
void pudown(int o,int l,int r){
int mid=(l+r)>>1;
if(tre[o].laz){
pdlaz(o<<1,mid-l+1,tre[o].laz);
pdlaz(o<<1|1,r-mid,tre[o].laz);
tre[o].laz=0;
}
if(tre[o].dlt){
pddlt(o<<1,mid-l+1,tre[o].dlt,1);
pddlt(o<<1|1,r-mid,tre[o].dlt,1);
tre[o].dlt=0;
}
if(tre[o].tag){
pdtag(o<<1,tre[o].tag);
pdtag(o<<1|1,tre[o].tag);
tre[o].tag=0;
}
}
void puhigh(int o){
tre[o].sum=tre[o<<1].sum+tre[o<<1|1].sum;
tre[o].hsum=tre[o<<1].hsum+tre[o<<1|1].hsum;
}
void updat(...){
if(L<=l&&r<=R){
pdlaz(o,r-l+1,val);
return;
}
}
int query(...){
if(L<=l&&r<=R){
return tre[o].hsum;
}
}
int main(){
while(T--){
//after modify do this:
pdtag(1,1);
}
return 0;
}
俊摊线段树
又名:主席树合并。
P3302 - [SDOI2013]森林
https://www.luogu.com.cn/problem/P3302
前置知识:如何做到树上查询第 k 小?
P2633 - Count on a tree https://www.luogu.com.cn/problem/P2633
很简单,一个点的主席树由其父亲继承而来。
设 \(T_x\) 为第 \(x\) 棵线段树。
类比区间上查询第 k 小是 \(T_r-T_{l-1}\),我们可以得到:
设我们要查 \(p,q\) 的第 k 小,则答案为 \(T_p+T_q-T_{lca}-T_{fa(lca)}\)。
那么如何 Link 两个点呢?直接暴力地 Link 即可,用启发式合并,每次更新的是较小的树,可以做到 \(O(\log n)\)(题解说法)。
(但是我觉得时间应该是 \(O(\log^2 n)\) 毕竟 1 每到一个点都要用 \(O(\log n)\) 时间插入主席树;2 要重构倍增数组才能求 LCA)