【学习笔记】线段树
满二叉树的节点个数是
1.扫描线
- 面积并(P5490【模板】扫描线)
不想贴图。
把这些矩形贴在一起,然后我们发现这些矩形的侧边把整个图割成了
所以我们考虑线段树维护每一个侧边被覆盖的长度,移到下一个侧边就是这个覆盖长度乘上移动长度。
线段树维护这个线段的贡献,以及被完全覆盖的次数。碰见左侧边就让对应区间加一,碰见右侧边就让对应区间减一。
维护这个线段的贡献和这个线段被完全覆盖的次数。
在 pushup 的时候我们如果被完全覆盖过就直接不管儿子了。因为查询只有整体,而且增加和删除是对应的,根本不需要 pushdown。
- 周长并(P1856 矩形周长)
和面积不一样的在于两个线段贴到一起中间那个是被消掉的。
所以多维护几个东西代表左右端点是否被覆盖以及这个线段有多少个不相交的线段。
2.线段树合并
首先我们可以把链上操作转成树上差分。然后我们对每个节点开一棵权值线段树。朴素的树上差分都是开一个桶然后加和,但是这里开的是线段树。
所以就有了线段树合并。
在把
就没了。
inline void merge(int x,int y){ if(!sg[x].ls&&!sg[x].rs&&!sg[y].ls&&!sg[y].rs){//并到叶子节点了直接两个合起来 sg[x].sm+=sg[y].sm; return; } if(sg[y].ls){//如果要并过来的没有左儿子就不用往左边合并了 if(sg[x].ls)merge(sg[x].ls,sg[y].ls); else sg[x].ls=sg[y].ls; } if(sg[y].rs){ if(sg[x].rs)merge(sg[x].rs,sg[y].rs); else sg[x].rs=sg[y].rs; } sg[x].sm=sg[sg[x].ls].sm+sg[sg[x].rs].sm;//更新 return; }
首先很容易看得出来单次合并的复杂度是
那么对于
注意线段树合并只能用于动态开点,因为普通线段树本来就是满的。
关于另一种带返回值的线段树合并,本质上就是返回更新后的节点编号,需要代码直接看 这份提交。
空间如果卡得紧可以试着用 回收节点。
至于 带转移的线段树 实际上也是一样的,把转移的东西放在合并里面,在某个节点为空的时候就代表转移转完了,打上一个 tag 之类的。注意合并的时候要 pushdown。
发现
Il int merge(int x,int y,int l,int r){ if(!x||!y)return x|y; int p=++si,mid=(l+r)>>1; if(l==r){sm[p]=sm[x]+sm[y];return p;} ls[p]=merge(ls[x],ls[y],l,mid); rs[p]=merge(rs[x],rs[y],mid+1,r); pup(p); return p; }
3.线段树分裂
在分裂函数里面传当前节点,就是一边分裂一边建树。复杂度和区间查询一样。
inline void split(int ql,int qr,int &x,int &y,int l,int r){ if(!x||qr<l||r<ql)return; if(ql<=l&&r<=qr){//就是要割的 y=x; x=0; return; } if(!y)y=build(); int mid=(l+r)>>1; if(ql<=mid)split(ql,qr,sg[x].ls,sg[y].ls,l,mid); if(qr>mid)split(ql,qr,sg[x].rs,sg[y].rs,mid+1,r); sg[x].sm=sg[sg[x].ls].sm+sg[sg[x].rs].sm; sg[y].sm=sg[sg[y].ls].sm+sg[sg[y].rs].sm; return; }
4.线段树优化建图
注意这个东西在最基本的情况下也有
我们考虑开线段树维护这个东西。令当前节点为
首先对于一个维护
然后因为子节点能够通往父节点,所以还要
因为是包含关系并不是实际边所以边权设为
很明显这样会出问题。
这样子我们任何一个叶子节点到根节点的权值都是
问题出在路径是双向的地方。我们尝试开两个线段树解决这个问题。
第一棵线段树
因为它们的叶子节点是同样的点,所以可以直接当同一个。我们尝试使用动态开点线段树,这样叶子节点的编号就是对应在数组的编号,比较好操作。
那么对于普通连边我们直接连就好。对于连区间的,我们搞到对应区间的编号然后和对应点连一连就好。
把每个点当成普通点跑就好了。
但是注意点向区间 连边 是
一开始连线段树的边数就有大致
当然我们还能再加强。
- 区间向区间连边。
直接连边复杂度是
但是我们可以开一个虚点然后这两个对应区间都向虚点连边。
点数最多加
5.线段树分治
注意它和线段树其实关系并不大,本质其实是分治思想,长得非常像线段树而且给了
一个图是二分图代表每条边两边端点染色不同。
显然除了 dfs 我们还有一种扩展域并查集的做法,对于每个点开正反两个点代表不同的颜色,合并边的时候直接把正
为什么用并查集?因为它支持插入,在使用可撤销并查集(带秩合并)可以做到
我们对于一个时间轴开线段树,每个点维护一个 vector 表示覆盖这个点的边集,那么每条边能够做到
我们考虑统一处理答案。
对于区间
注意我们开栈维护加入的边然后在分治退出的时候把这些边一一删掉就可以更好的维护(?)。
其实线段树分治是在分治的时候维护一个其它的数据结构并且能够通过
6.李超树
用于维护直线,在斜率之类的题目里面算是超级有用的存在。
我们考虑维护斜率,对于线段树节点
更新时因为不好覆盖,tag 需要一直传下去。假设更新 tag 时更新的值比原节点不优(如果更优可以把两个一次函数交换,因为原节点也没有下传)。然后我们根据其能否在左右端点造成更优的贡献决定是否继续下传。
因为数学不好所以给个原因。对于左端点而言,因为中点已经不优,如果左端点也不优,代表这个一次函数在
这个东西的复杂度是 pushdown
是单点查询的复杂度的。
如果插入线段是
运用了标记永久化的思想,查询时记得边走边查。
李超树 支持合并,做法是无论是否是叶子节点都要把 y 对应的直线在 x 里进行 pushdown(这里假设目标是把 y 并到 x 上)。注意一定要先把左右儿子更新再传直线,否则可能出现左右儿子更改导致 pushdown 失效的情况。由于注意到一条直线如果下传那么最多下传
7.套矩阵
只是一种 trick。由于矩阵需要满足结合律,线段树刚好也可以满足结合律,所以我们可以把线段树和矩阵套起来维护。
转移时注意矩阵没有交换律是左儿子乘右儿子还是右儿子乘左儿子。
递归里面传矩阵作为变量常数过大,可以把矩阵在线段树外面更新然后更新的时候只需要传位置。
8.势能线段树
重点在于势能的分析,会把遇到的在能力范围内可以分析势能的东西堆在这里。有些分析是自己胡的,错了不管。
8.1 区间开根
定义势能函数
注意到一个数
记录一个区间的最大值,若是
势能为
8.2 区间对一个数取 gcd
如果对于
只需要支持快速判断一个区间内是否所有数都能被一个数整除。维护 lcm 即可。维护 lcm 需要给复杂度多乘上一个
8.3 区间取 min(取 max 同理)
对于线段树节点维护最大值
对于一次取 min 操作:
-
若
发现不需要更改。 -
若
,发现只需要更改 的位置,在这个位置上打一个 tag 然后依据我们维护的最大值个数更新需要维护的比如区间和之类的东西。对于这里的 tag 可以在 pushdown 的时候让子节点根据父节点维护的东西修改,就没有必要记录一堆东西用于 pushdown 还容易出错。 -
若
递归下去更改。
前面两种操作的复杂度与普通线段树是类似的。
注意到最后一种操作会使得这一段的颜色个数至少减一。定义势能函数
复杂度
8.4 区间推平
对的这个玩意势能是正确的。那为什么隔壁 ODT 要数据随机,不懂。
你考虑维护这段区间是否全为一个颜色,如果是是哪个颜色。然后你修改的时候只有在这一段都是一个颜色时才打上修改 tag。
一次修改最多使颜色段个数增加
如果你对于一个包含在查询区间的段里面递归下去推平,注意到这一段的颜色个数至少减一,对应到线段树上就是有
那么复杂度就是
8.5 区间推平,区间整除一个数
考虑没有区间推平操作,一个数在特判
然而一次推平操作会把每个点的势能都加上
于是把区间内数相等的区间的势能看作
一次推平只会有
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!