线段树(合集)
0. 树?睡蕉小猴!
因为以前的线段树写成了好几个 blog,所以这里写一个合集。
1. 线段树
1.1 线段树 1
线段树是用来处理一类“区间修改+区间操作”的问题的数据结构。
1.2 线段树的节点与性质
建立一棵二叉树,根节点为
由二叉树,我们知道节点
定义
规定:若
定义:
那么线段树到底长成了什么样子呢?如图:
观察线段树:
- 每个点要么有
个子结点,有么为根。 - 有
个节点。 - 高为
。
1.3 建立一颗线段树
有一个显然的性质,
那么定义合并函数 pushup(int u)
:
void pushup(int u) { w[u]=w[u*2]+w[u*2+1]; }
那怎么对于一个序列
void build(int u,int l,int r) { if(l==r) { w[u]=a[l]; return; } int mid=(l+r)/2; build(u*2,l,mid); build(u*2+1,mid+1,r); pushup(u); }
代码很好理解。
1.4 区间查询
我们知道查询区间为
代码如下:
bool InRange(int L,int R,int l,int r) { return (l<=L)&&(R<=r); } bool OutofRange(int L,int R,int l,int r) { return (L>r)||(R<l); } long long query(int u,int L,int R,int L,int r) { if(InRange(L,R,l,r)) return w[u]; else if(!OutofRange(L,R,l,r)) { int m=(L+R)/2; return query(u*2,L,m,l,r)+query(u*2+1,m+1,R,l,r); } else return 0; }
1.5 懒标记与区间查询
定义:懒标记(
一个班的学习委员知道语文作业后,不用立即告诉下面的组长们,可以再等一下数学作业。(修改时不直接下方标记)
懒标记具有可合并性,比如语文张试卷,数学 张试卷,那么总共 张试卷。
如果老师问学习委员作业写完没有,只需要自己写完即可。(不涉及叶子节点不下方标记)
知道所有作业后也不需要立刻告诉大家,老师收作业时再告诉大家即可。(查询叶子节点下方懒标记)
那么区间修改和区间查询可以这么写:
void maketag(int u,int len,long long x) { lzy[u]+=x; w[u]+=len*x; } void pushdown(int u,int l,int r) { int m=(l+r)/2; maketag(u*2,m-l+1,lzy[u]); maketag(u*2+1,r-m,lzy[u]); lzy[u]=0; } void update(int u,int L,int R,int l,int r,long long x) { if(InRange(L,R,l,r)) maketag(u,R-L+1,x); else if(!OutofRange(L,R,l,r)) { int m=(L+R)/2; pushdown(u,L,R); update(u*2,L,m,l,r,x); update(u*2+1,m+1,R,l,r,x); pushup(u); } } long long query(int u,int L,int R,int l,int r) { if(InRange(L,R,l,r)) return w[u]; else if(!OutofRange(L,R,l,r)) { int m=(L+R)/2; pushdown(u,L,R); return query(u*2,L,m,l,r)+query(u*2+1,m+1,R,l,r); } else return 0; }
1.6 封装线段树
初学阶段千万不要复制,等你自己能在至多
const int maxn=500006;//依情况修改 struct Segment{//依情况修改 long long a[maxn],w[4*maxn],lzy[4*maxn]; void pushup(int u) { w[u]=w[u*2]+w[u*2+1]; } void build(int u=1,int l=1,int r=n) { if(l==r) { w[u]=a[l]; return; } int m=(l+r)/2; build(u*2,l,m),build(u*2+1,m+1,r); pushup(u); } bool InRange(int L,int R,int l,int r) { return (l<=L)&&(R<=r); } bool OutofRange(int L,int R,int l,int r) { return (L>r)||(R<l); } void maketag(int u,int len,long long x) { lzy[u]+=x; w[u]+=len*x; } void pushdown(int u,int l,int r) { int m=(l+r)/2; maketag(u*2,m-l+1,lzy[u]); maketag(u*2+1,r-m,lzy[u]); lzy[u]=0; } int query(int l,int r,int u=1,int L=1,int R=n) { if(InRange(L,R,l,r)) return w[u]; else if(!OutofRange(L,R,l,r)) { int m=(L+R)/2; pushdown(u,L,R); return query(l,r,u*2,L,m)+query(l,r,u*2+1,m+1,R); } else return 0; } void update(int l,int r,long long x,int u=1,int L=1,int R=n) { if(InRange(L,R,l,r)) maketag(u,R-L+1,x); else if(!OutofRange(L,R,l,r)) { int m=(L+R)/2; pushdown(u,L,R); update(l,r,x,u*2,L,m); update(l,r,x,u*2+1,m+1,R); pushup(u); } } };
定义:Segment a;
使用:构造 a.build()
,区间修改 a.update(x,y,k)
,区间查询 a.query(x,y)
。其中,
1.7 开关
这里很简单,区间异或就是将
void maketag(int u,int len,long long x) { lzy[u]^=1; w[u]=len-w[u]; }
1.8 线段树适用范围
可以发现,线段树一定可以从
比如区间众数便不能由线段树维护。而区间异或,区间 GCD 都可以用线段树维护。
1.9 线段树 2/多标记处理
这题需要建立两个标记:加法标记和乘法标记。
这时候需要注意标记下方顺序。
详见此题题解。
1.10 复杂度分析
空间复杂度
时间复杂度:建树
2. 动态开点线段树
这是线段树最简单的拓展。
请在理解完线段树后再来看此节。
2.1 适用范围
当操作区间很大(
2.2 动态开点的本质
观察线段树,单次操作最多使用的点为
那么实际上整棵树最多使用的点为
一般来说,
所以动态开点只记录这些节点,最多只需要开
2.3 动态开点的实现 / 封装动态开点线段树
用一个 node
存储数据:
struct node{ int l,r,val,lzy; node(){ l=r=val=lzy=0; } }tr[30*maxm];
pushdown
操作时,除了更新
void pushdown(int u,int l,int r) { if(!tr[u].l) tr[u].l=++tot; if(!tr[u].r) tr[u].r=++tot; int mid=(l+r)/2; maketag(tr[u].l,mid-l+1,tr[u].lzy); maketag(tr[u].r,r-mid,tr[u].lzy); tr[u].lzy=0; }
初始时只需要开一个根节点即可。其余操作类似。
#define maxm 500005 int n=2000000000; struct dt_Segment_tree{ struct node{ int l,r,val,lzy; node(){ l=r=val=lzy=0; } }tr[30*maxm]; int tot=0; dt_Segment_tree(){ tot++; } void pushup(int u) { tr[u].val=tr[tr[u].l].val+tr[tr[u].r].val; } void maketag(int u,int len,int x) { tr[u].lzy+=x; tr[u].val+=len*x; } void pushdown(int u,int l,int r) { if(!tr[u].l) tr[u].l=++tot; if(!tr[u].r) tr[u].r=++tot; int mid=(l+r)/2; maketag(tr[u].l,mid-l+1,tr[u].lzy); maketag(tr[u].r,r-mid,tr[u].lzy); tr[u].lzy=0; } bool InRangeOf(int L,int R,int l,int r) { return (l<=L)&&(R<=r); } bool OutRangeOf(int L,int R,int l,int r) { return (L>r)||(R<l); } void update(int l,int r,int x,int u=1,int L=1,int R=n) { if(InRangeOf(L,R,l,r)) maketag(u,R-L+1,x); else if(!OutRangeOf(L,R,l,r)) { pushdown(u,L,R); int mid=(L+R)>>1; update(l,r,x,tr[u].l,L,mid); update(l,r,x,tr[u].r,mid+1,R); pushup(u); } } int query(int l,int r,int u=1,int L=1,int R=n) { if(InRangeOf(L,R,l,r)) return tr[u].val; else if(!OutRangeOf(L,R,l,r)) { pushdown(u,L,R); int mid=(L+R)>>1; return query(l,r,tr[u].l,L,mid)+query(l,r,tr[u].r,mid+1,R); } else return 0; } }a;
2.4 【模板】动态开点线段树
模板题,没什么说的。
3. 树套树
阅读本章前请先完成【模板】动态开点线段树。
3.1 什么是树套树
考虑这样一个问题:
- 在一个
的平面上,维护任意一个平面 的区间覆盖与区间查询。
这题不可以简单的二维树状数组,必须使用线段树。
3.2 线段树套线段树
最简单的树套树。
有一个暴力的想法:我们对于每一行开一个线段树。这样单次操作时间复杂度
那怎么优化呢?容易发现,对于每一行,我们也可以看成区间加。
那么建一颗线段树,每个线段树的结点都是一颗线段树,那么时间复杂度
但是如果直接这么建是
考虑动态开点,这样每一次只会增加
空间时间复杂度就都是
4. 可持久化线段树
5. 扫描线
本文作者:sLMxf
本文链接:https://www.cnblogs.com/SLMXF/p/18564527
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步