线段树 & 树状数组
线段树
常用于维护区间值
代码
和题解有很大差异,但是过了就好
void Pushup(int x) {
s[x]=(s[x<<1]+s[x<<1|1]);
}
void Pushdown(int x,int l,int r) {
s[x]=(s[x]+ad[x]*(r-l+1));
if(l!=r) ad[x<<1]=(ad[x<<1]+ad[x]);
if(l!=r) ad[x<<1|1]=(ad[x<<1|1]+ad[x]);
ad[x]=0;
}
void Build(int x,int l,int r) {
if(l==r) {
s[x]=a[l];
return;
}
int mid=(l+r)>>1;
Build(x<<1,l,mid);
Build(x<<1|1,mid+1,r);
Pushup(x);
}
void Plus(int x,int l,int r,int k,int L,int R) {
Pushdown(x,l,r);
if(l>=L&&r<=R) {
if(l!=r) ad[x<<1]=(ad[x<<1]+k);
if(l!=r) ad[x<<1|1]=(ad[x<<1|1]+k);
s[x]=(s[x]+(r-l+1)*k);
return;
}
int mid=(l+r)>>1;
Pushdown(x<<1,l,mid);
Pushdown(x<<1|1,mid+1,r); //注意这两句
if(L<=mid) Plus(x<<1,l,mid,k,L,R);
if(R>mid) Plus(x<<1|1,mid+1,r,k,L,R);
Pushup(x);
}
long long Quary(int x,int l,int r,int L,int R) {
Pushdown(x,l,r);
if(l>=L&&r<=R) return s[x];
long long ans=0;
int mid=(l+r)>>1;
if(L<=mid) ans=(ans+Quary(x<<1,l,mid,L,R));
if(R>mid) ans=(ans+Quary(x<<1|1,mid+1,r,L,R));
return ans;
}
实现细节
线段树开4倍大小的原因是,一些数(如5)可能使得层数多一层
Pushup的时候俩儿子的懒标记必须传干净
Pushdown的时候注意不要让叶子节点访问子节点,不然会RE
Pushdown用法总结:
1、访问到一个节点时,啥也不干,先Pushdown消除懒标记,使得后面的分析更容易;
2、Plus函数在访问完两个儿子后,要Pushup使得x数值正确,本来两儿子Add的Pushdown可以使得懒标记消掉,但是会有儿子进不了Add,因此需要在外面Pushdown一次;
3、Pushup不会对懒标记进行操作,故Pushup前先Pushdown两个儿子保证数值正确;
代码调试
对一个区间反复加和往往能发现问题(如[1,2],[1,1],[2,2],[1,3],[2,3]……)
当然,如果能背版是最好的
区间除法
转换成减法 未解决
区间开方
区间gcd
区间第一个值小于k的位置
区间最大子段和
维护区间和,最大前缀和,最大后缀和,最大子段和即可
例:小白逛公园
区间限长最大子段和
区间历史版本和
维护从开始到当前时刻的
一种思路是在后一个加标记遇到前一个加标记时,下推前一个加标记统计贡献
发现这种方法意味着加标记不可合并,会退化成
进一步思考发现:并不是不能合并,考虑前一个标记存在时的历史和:
这时
因此另开一个标记维护即可
大数比较
对于
例题:The Classic Problem 可持久化即可
扫描线
用于解决二维数点问题
模型:对于
线段树实现
设
实现时发现
思考发现不同于正常线段树,
例题
P1502 窗口的星星
直接考虑矩形的价值不可行
正难则反,考虑每个点的贡献,发现每个点对一个矩形内的点有贡献,贡献转移至矩形,扫描线
李超线段树
树状数组(BIT)
参考自bestsort的博文
定义
树状数组本质上为线段树的优化,与线段树相比,它的优点在于代码简洁,时间效率高(线段树固定是
以下是各路收集的优质理解:
线段树的变形
在用线段树求前缀和时,发现访问的节点均为左节点,大量节点弃置不用,造成时间和空间的浪费
如图,灰色节点永远用不上
于是对线段树进行改进,丢掉无用点,发现剩下的节点刚好就能放进原数组里,于是原数组就被改造为树状数组
基于二进制的高效访问
空间已经优化完了,但如果还是像线段树一样从根向下一层一层跑,那就优化了个寂寞
于是有人发现了树状数组的访问规律
lowbit(x):取出x二进制位中最低位的1(注意取出的是1<<(i-1)不是位数i)
发现在查询时,只要不断丢掉最低位的1,就可以访问到全部前i个元素
而在单点修改时也类似,只要不断加上最低位的1,就可以完成管辖元素i的所有节点的更新
下图更清晰
另一种理解:区间求和
观察树状数组,发现对于节点i,其实管辖的是右端点为i,长度为lowbit(i)的区间,通过i-=lowbit(i),可以快速跳往前一个区间,达到不重不漏地访问前i个元素,易知跳转操作次数由i的二进制表示中的1的个数决定,这就是树状数组的时间复杂度最坏为
这种理解在分析二维树状数组中会很方便
单点修改,区间查询
如上文
这里求lowbit(x)用了一种基于机器编码的方法,具体证明见bestsort原文
int n,c[MAXN]; //长度为n的树状数组
int lb(int x) {return x&-x;}
void Add(int x,int k) {
for(int i=x;i<=n;i+=lb(x))
c[i]+=k;
}
int Quary(int x) {
int ans=0;
for(int i=x;i;i-=lb(x))
ans+=c[i];
return ans;
}
Add(x,k); //给元素x加上k
Quary(y)-Quary(x-1); //[x,y]求和
区间修改,单点查询
考虑利用差分数组,这样前i个数的和就是元素i,而区改也能快速实现
//前面一样
Add(x,k); Add(y+1,-k); //[x,y]加k
Quary(x); //求元素x
区间修改,区间查询
考虑区改单查的变形
前x个元素的和为(d[]为差分数组)
记录两个数组
int n,d[MAXN],s[MAXN];
int lb(int x) {return x&-x;}
void Add(int x,int k) {
for(int i=x;i<=n;i+=lb(x))
d[i]+=k,s[i]+=k*x;
}
int Quary(int x) {
int sumd=0,sums=0;
for(int i=x;i;i-=lb(x))
sumd+=d[i],sums+=s[i];
return (x+1)*sumd-sums;
}
Add(x,k); Add(y+1,-k); //[x,y]加k
Quary(y)-Quary(x-1); //[x,y]求和
树状数组二分
和线段树一样,树状数组也可以二分
上面提到的一种定义
由于lowbit(i)为
令
二维树状数组
类比一维树状数组,c[x]表示右端点为x,长度为lowbit(x)的区间,则c[x][y]表示右下角坐标为(x,y),长lowbit(x),宽lowbit(y)的矩阵,类推转移即可
(懒得写了,现场推吧)
模型:求区间元素种类数
[P1972] HH的项链
树状数组可以快速地统计前缀和,但是遇到相同元素就会重复统计种类
由于在[l,r]中,第i种颜色只能在最后一个该颜色删除后才会消失,所以只需维护每种颜色最后出现的位置即可
01树状数组存储,st数组记录每种元素最后出现位置,若有相同元素进入,则删掉该位的1(没有贡献),更新st[]
权值线段树 & 权值树状数组
在统计个数问题上(三维偏序)有优势
求区间第k小值
权值线段树+二分即可,时间复杂度为
这是主席树的底层原理之一
动态开点线段树
细节
一般开到
注意与动态开点线段树有关的题目,自己分析复杂度正确时TLE,有可能是炸数组了(普通线段树是RE)
线段树合并
对于一棵树,如果每个点只有
思路:类似于主席树,可以想到链上的该问题主席树可以维护,而到了树上,就考虑线段树合并
有两种写法,时间复杂度都是
写法1:一次性
对于树
空间复杂度大概为
细节
线段树合并时,如果这么写:
int mid=(l+r)>>1;
if(!lc[now]&&lc[pre]) lc[now]=lc[pre];
if(lc[now]&&lc[pre]) Merge(lc[now],lc[pre],l,mid);
if(!rc[now]&&rc[pre]) rc[now]=rc[pre];
if(rc[now]&&rc[pre]) Merge(rc[now],rc[pre],mid+1,r)
那么在进入第一个if后,lc[now]获得值,进入第二个if,导致出错,因此要用else分隔
写法2:可持久化
先和写法1一致,到并下一棵树时,如果有点值变化的点是
空间复杂度大概为
复杂度分析
待办
应用
P4556 [Vani有约会]雨天的尾巴 /【模板】线段树合并
#3628. 「2021 集训队互测」树上的孤独
补充
对于支持线段树合并的题,都支持树链剖分(因为链和子树都在
前者时间较优,后者空间较优
主席树
主席树即为可持久化线段树,思路就是对每一个历史版本都开一棵线段树,但是这样时间复杂度会很高。
由前面线段树合并可知,当点数较少时,用动态开点线段树比较优,则对于版本
求区间第k小值
沿用之前历史版本的思路,对每一个
可持久化数组
实际上为主席树的弱化版:对于单点修改的数组建立主席树,实际上就是只有叶子挂东西的线段树
细节
可持久化trie中叶子节点的更新是s[x]=s[pre]+1
,不是s[x]++
——这样写会丢失前面相同的值
P3293 [SCOI2016]美味
思考发现没有数据结构可以同时高效维护异或和加,考虑分位
我们从高位到低位考虑:要想取最大值,就要使高位尽量为
时间复杂度玄学优先级
树套树
树套树实际上是一种思想——线段树的结点并不只能挂值,可以挂线段(李超树),线段树,平衡树,堆等等
一个经典问题就是二维线段树——对行开一个线段树,结点上挂统计处理
有时候第二维不一定是
由于线段树只有
基于满二叉树的数据结构
zkw线段树
非递归版线段树,具有优秀的常数
考虑将一棵线段树底层结点补成
所以这棵线段树可以从下往上跑(普通线段树都是从上往下)
区间修改
由于从下往上跑,区间标记不好下传,考虑标记永久化
在往上跑时,如果自己的兄弟也在区间内,给兄弟也打上标记
区间查询
由于标记永久化,所有的询问都得跑到根,以获取全部标记(类似李超树)
猫树
满二叉树维护无修改区间查询,预处理复杂度
先补全数组至
询问时跳到
猫树和zkw线段树底层思想差不多,都是基于快速访问下标来优化时间,但猫树和普通线段树结构又有区别——猫树空间是
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下