线段树
基本操作
线段树可以完成许多序列上相对位置不变的操作
比如区间加,乘,max,gcd,开平方,矩阵,线性基等等……
对于区间 \(gcd\),复杂度暂未明,可以支持区间加,具体做法用线段树维护差分数组的 \(gcd\) 即可
大部分操作只需要在 \(update\) 处做手脚模拟题意即可
对于懒标记,仔细思考是否需要,比如区间 \(max\) 等不需要懒标记,一些题加入后可能会算重(比如扫描线)
比如 这个题,取 \(min\) 和 \(max\) 的操作比较绕,具体操作时可以发现其实没有必要开懒标记,直接将父亲区间的 \(min\) 和 \(max\) 作为需要下传的懒标记即可
结构化分治过程
对于单次操作可以分治并且合并出的信息可控的题可以用线段树维护
比如位运算、\(gcd\)、随机数据下的凸包等
gcd版本,位运算版本找不到了
线段树维护单调栈
比如 P4198 楼房重建
需要维护出每次操作后单调栈内元素的个数
发现合并时不好合并
需要维护的信息有长度,最大值
那么考虑右区间有多少可以合并进左区间,即序列的开始要比左区间的最大值大,那么在右区间上二分即可
可以发现拿着左区间的最大值去右区间里找,分类讨论后还是只有一条路径
前面的转化就不写了
这道题的不同之处在于单调栈是从询问右端点向左延伸的,这样一来区间内单调栈信息不能直接预处理
只能对每个区间预处理出左子区间的信息,然后把询问划分成 \(log\) 段后再次向左递归去寻找相应的分界点
权值线段树
以权值为下标建立线段树,在一些题中非常好用,可以支持第 \(k\) 大等的操作
注意权值下标是否包含零,以及一个节点上多个数计算和时需要判一下
扫描线
扫描线解决平面上矩形类问题,比如面积或周长,以及一些题目可以抽象成扫描线
扫描线的 \(update\) 比较特殊,并且需要注意一下左右端点
对于扫描线,更广义的理解可以是对于序列上的区间询问,可以按照一维排序后,移动端点的过程中动态维护另一维的答案,这个会非常常见
这个线段树的 \(update\) 并不是那么常规,放个代码吧(毕竟现在写一个小时才过……)
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=2e5+5;
int n,x,y,xx,yy,tot,cnt,ans,lsh[maxn];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
#define mid (t[p].l+t[p].r>>1)
struct Node{int pos,l,r,op;}seg[maxn];
struct Seg{int l,r,cnt,sum;}t[maxn<<2];
void build(int p,int l,int r){
t[p].l=l,t[p].r=r;if(l==r)return ;
build(p<<1,l,mid),build(p<<1|1,mid+1,r);
}
void update(int p){t[p].sum=(!t[p].cnt?t[p].l==t[p].r?0:t[p<<1].sum+t[p<<1|1].sum:lsh[t[p].r+1]-lsh[t[p].l]);}
void spread(int p,int w){t[p].cnt+=w;update(p);}
void change(int p,int l,int r,int w){
if(t[p].l>=l&&t[p].r<=r)return spread(p,w);
if(l<=mid)change(p<<1,l,r,w);
if(r>mid)change(p<<1|1,l,r,w);
return update(p);
}
int get(int x){return lower_bound(lsh+1,lsh+tot+1,x)-lsh;}
signed main(){
n=read();
for(int i=1;i<=n;i++){
x=read(),y=read(),xx=read(),yy=read();
seg[++cnt]={y,x,xx,1};
seg[++cnt]={yy,x,xx,-1};
lsh[++tot]=x,lsh[++tot]=xx;
}
sort(lsh+1,lsh+tot+1);tot=unique(lsh+1,lsh+tot+1)-lsh-1;
sort(seg+1,seg+cnt+1,[](Node a,Node b){return a.pos<b.pos;});
build(1,1,tot);
for(int i=1;i<cnt;i++){
seg[i].l=get(seg[i].l),seg[i].r=get(seg[i].r);
change(1,seg[i].l,seg[i].r-1,seg[i].op);
ans+=(seg[i+1].pos-seg[i].pos)*t[1].sum;
}
cout<<ans;
return 0;
}
- 维护圆
考虑从左到右扫描,同时把上下半圆都加入一个 \(set\) 里
同时每个上半圆插入的时候,先查询其上第一个圆弧,如果是上圆弧,那么直接认父
否则认作兄弟,把他的父亲当成父亲即可
注意一定要把圆弧整个记录下来,选取其上的某一个点均是不能考虑完全的
动态开点
一些题常常值域很大,或者每棵线段树上的点很稀疏,那么可以采用动态开点的方式
一类题目是多次询问选取给定操作序列的区间后的变换情况,如果满足单调性可以使用扫描线+动态开点线段树维护
线段树合并与分裂
一些题需要在树上维护颜色等信息,存入线段树后比较稀疏,由于需要有儿子向父亲汇总信息的过程,需要合并两棵线段树
线段树的分裂类似于非旋 \(treap\),还是比较好实现的,然而目前并没有找到什么正经用途
线段树合并可以优化树形 \(dp\) 转移
这道题的式子写出来,形如 \(f[x][i]=f[x][i](sum[y][dep_x]+sum[y][i])+f[y][i]sum[x][i]\),那么用线段树为 \(f[x]\),考虑线段树合并时从左到右合并,那么合并到位置 \(i\) 的时候顺便就可以把前缀和处理出来了
均摊复杂度
目前还没有搞懂 \(seg-beat\) 这个名字是啥意思
首先是根号、欧拉函数等会规律变化的变换,可以证明复杂度均正确
更一般的情况,本质上把这些操作都变成加法操作会使得许多问题变得易于解决
这道题的操作是下取整
当所有数都相同时同时下取整否则递归,在一般情况下复杂度正确
但是当极差为 \(1\) 的时候可能下取整之后不会缩小
但是考虑把相同数的下取整看成加法标记,那么极差不缩小的情况也是涵盖其中的
在一种题目中,区间 \(min\) 与区间加、区间和同时出现,普通做法很不好解决
还是把取 \(min\) 加法化
维护最大值和次大值,一个区间当取 \(min\) 值介于次、最大值之间再去处理,否则递归
这样在保证复杂度的情况下又把操作转化为了加法,所有的事情就变得好做起来
这个东西还可以扩展到 \(k\) 个序列的情况,需要对于所有大小关系维护出 \(2^k\) 套标记
笛卡尔树的子树大小就是作为最大值区间的大小,即 \(pre-suf-1\)
这个东西将 \(pre\) 和 \(suf\) 分开维护
每次从小到大插入一个数,设其位置为 \(pos\)
那么把左边的数 \(suf\) 设为 \(pos\),把右边的 \(suf+1\)(因为是动态插入,整体移位了)
可以发现回归到上面维护的东西
一定注意这个东西的懒标记在下传的时候只下传到取得最大值的子树!
历史最值
首先是较为简单的情况,支持
- 区间加
- 区间历史最值
需要维护当前最值,历史最值,当前加法标记,加法历史最大标记
每次用当前最值+历史标记更新历史最值
用加法标记+历史标记更新历史标记
接着加入区间 \(min\) 和区间和,此时用上面所说的方法把 \(min\) 转化成加法标记
由于需要维护区间和,那么还得关心非最大值的取值问题
那么再维护两个懒标记,表示非最大值加法标记,非最大值历史加法标记
维护后者是因为在 \(spread\) 的时候对于非最大值子树需要用它来更新
代码见 P6242 【模板】线段树 3
接着加入区间覆盖操作,大体类似,其核心思想是把覆盖后的加法操作也看成覆盖操作,维护历史覆盖的最大值,详见 P4314 CPU 监控
考虑维护一种标记 \((a,b)\) 表示先加 \(a\) 再赋值为 \(b\)
可以发现前三种操作都可以通过适时添加 \(inf\) 迎刃而解
标记的叠加
\((a_1,b_1)\) 和 \((a_2,b_2)\) 合并成 \((a_1+a_2,max(b_1+a_2,b_2))\)
标记的取 \(max\)
\(max(a_1,a_2),max(b_1,b_2)\)
考虑把空间与时间翻转,变成倒序枚举位置并更新相应时间的最小值
此时询问只需要维护当前值变化的次数即可
考虑对于一个连通块,放在其最浅的那个节点处理,于是修改变成了对一个点子树中和自己同色联通的加法
考虑一个子树在线段树上是一个区间,那么子树中最浅的那些异色连通块子树也是一个区间,考虑用线段树的科技做到 \(spread\) 的时候可以把异色区间屏蔽掉
那么我们规定 \(spread\) 时只往和自己颜色相同的儿子推,\(update\) 的时候也只上传同色儿子
是否同色的判定可以通过区间中点到根路径上黑色点个数的 \(min\) 作为标准判定
历史版本和
维护这样几个标记:hsum
历史版本和,lazyh
历史和懒标记,lazy
加法懒标记,tim
未更新的时长,sum
区间和
void spread(int p,int lazy,int tim,int lazyh){
t[p].hsum+=t[p].sum*tim+lazyh*t[p].len;
t[p].lazyh+=t[p].lazy*tim+lazyh;
t[p].sum+=t[p].lazy*t[p].len;
t[p].tim+=tim;t[p].lazy+=lazy;
}
首先来刻画出来需要满足的条件:\((max-min)-(r-l)=0\),那么对于每个右端点动态维护出左端点的这个信息,可以发现无论何时是全局最小值的那些点是满足条件的
可以发现最小值条件比等于条件更容易维护
\(min\) 和 \(max\) 这个条件可以用单调栈实现,具体地在弹栈的时候将权值差区间加在线段树上
由于要维护子区间的个数,相当于是时间轴上的历史版本和,那么需要维护出每个点作为最为全局最小值出现了几次,那么维护一个时间标记,每一轮都从树顶往下 \(spread\),注意只 \(spread\) 其 \(min\) 是当前区间 \(min\) 的子树
由于要维护区间覆盖下的历史值之和,那么看成了一个关于时间的一次函数,用线段树维护这个一次函数即可
一个省去线段树上二分的写法是只有左右儿子最值相等的时候才更新当前区间
一类关于全局最值的维护方式
如果要求数类似于点对产生的最值一类的问题,可以定义这些点对的顺序,有时可以根据题目性质使得状态数减少
题意:单点修改,全局 \(max_{|i-j|\le k}a_i+a_j\)
像这道题的顺序就是只维护 \(a_i\) 在自己范围内是最大值的那些 \(i\) 的位置即可,可以发现不会有统计不到的情况
注意规定等号取到的条件只能在一侧
题意:多次区间询问满足 \(y-x\le z-y\) 的三元组 \(a_x+a_y+a_z\) 的最大值
这道题的顺序就是 \(x,y>[x+1,y-1]\),同样可以发现不会有遗漏
而这个类似于单调栈的相邻区间,可知共有 \(O(n)\) 个
那么扫描线的同时维护这些对的区间贡献即可
主席树
是强制在线下解决二维问题的工具,相当于是把扫描线时线段树的所有版本均保存下来了
区间加区间和
考虑将加标记标记永久化
那么只需要把所有的 \(update\) 及询问方式改为 \((sum[r]-sum[l-1])\times t[p].lazy+t[p].s\) 即可
李超线段树
用于维护线段的线段树
具体地每个区间存在 \(mid\) 处最优的线段,同时考虑每个线段,如果在 \(l\) 处比最优线段大的往左递归,否则往右递归
一些扩展:如果询问的函数 \(f(x,y)\) 满足对于 \(x\le x',y\le y'\) 有 \(f(x,y)\le f(x',y')\),如果数据随机,那么在线段树上进行查找的时候,可以看 \(f(r,max(rs))\) 是否大于 \(ans\),如果大于就进去找,出来以后再看左边的
因为在随机数据下会更新 \(log\) 轮,而一次寻找都可以精确找到一个更优的,有 \(log^2\) 的复杂度
详细见 决战圆锥曲线