问题:
已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数加上 k。
2.求出某区间每一个数的和。
最简的方法是使用 for 循环,每一次更改和询问都去将数组更新一遍
但这种做法的时间复杂度达到了O ( n² ),如果想要AC拿满分,线段树是必不可少的
线段树前身-分块思想:
如图,这是一个数组,我们将它3个3个分成一块,每一个块储存着其下方对应的值的和
在开始使用之前,我们需要先将每个块初始化。以这个问题为例,每一个块中保存的是对应范围的和
若要给蓝色区间加上某个值
我们便在上方的 满足一整块的“块” 中打上标记,整块加上那一个值,而不满整块的区间则单独加上那个值。
若现要查询粉色区间的和,只需要将C(上一次步骤已经整体加过了某个数)、D、E三段相加,便可用较小的时间复杂度解决这个问题
这样过后原本需要四次操作的问题被我们简化成了两次(一个“块”+ 三个单个值)
但是分块思想的上限呢?是每 sqrt(n)分一块,最劣情况下的时间复杂度是 O(sqrt(n))(n的开平方)
那么...有没有更快的呢?
线段树-分块套分块
如图,这是一个线段树,可以看到,它是由很多的线段(其实就是“块”)组合而成的。大的线段又二分下去成为更短的线段,直到每条线段仅剩下一个值
在开始使用之前,我们需要先将线段树初始化。以这个问题为例,每一条线段需要维护的是它的区间值的和。
那么对于每一条线段,它的值就为它的 left_son 的值与它的 right_son 的值相加
当一条线段的区间只有一个值的时候,它的值是它数组对应的那个值
知道线段树的基本逻辑之后,我们便可以开始操作
若我们要给粉色区间的每个数加上某个值,可以给区间最上方的整段的线段打上标记(lazy_tag),这样下次询问时,我们只需知道
如图,我们在更改时只需要给这些粉色的线段记上标记,再更新这些粉色线段上方的线段中的值
(因为线段树是树形结构,使用dfs实现 初始化、修改、查询...,回溯时会顺带更新到上方的值),效率提升了不少
那么查询时会不会出什么问题呢?
如图,我们这时想要查询蓝色区间的值,对应到下方就是线段 A 与线段 B 的值相加
但是此时我们会发现,线段 A 由于下方的修改,更新往上传递,它的值是没问题的
但是线段 B 就不同了,它的上方线段被记上懒标记,但仅仅止步于上方,B 线段本身是没有被更新到的
这个时候我们就必须使用一些手段让 B 线段也拥有这个标记,才能保证查询准确
这就是懒标记的下放
如图,我们在查询时,dfs会往下走。在往下走的同时,我们将路径上每一条线段的懒标记下放
(一条线段下放懒标记只会更新到它下方的线段,并不会因为自身失去了懒标记而改变值)
(同时,在更改的时候也不要忘记pushdown!!这可以避免冲突)
下放过后,原先线段的懒标记被转移到它的 left_son 和 right_son 上,使他们更新自身值,最大程度地减少了时间复杂度,解决了此问题
最后贴上码满注释的贴心板子代码(c++)
#include <bits/stdc++.h>
#define ll long long//好的define可以大大减少代码量
#define mid ((l + r) / 2)
#define lson t * 2, l, mid//由于线段树是经典的二叉树,所以t的左儿子的编号为t*2,左儿子的区间自然是t的左区间至t的区间的中间(mid)
#define rson t * 2 + 1, mid + 1, r//右儿子同理
using namespace std;
const ll N = 1e6 + 1;
struct SegTree {
//记得要开四倍空间,我们老师的(划掉)血的教训
ll val[N * 4],y[N * 4];//val代表每个线段的值,y代表每个线段的 懒标记
//顺便不要学我这个lazy的命名方法,当时抽了才用了y这个字母,记得用容易标识,同时比较简短的变量名,比如 tag
void Pushup(ll t) {
val[t] = val[t * 2] + val[t * 2 + 1];//传入t,由t的左儿子和右儿子更新t的值
}
void Build(ll *v, ll t, ll l, ll r) {//初始化线段树
if (l == r) {//区间内只有一个值,那它的区间和必定是他自身
val[t] = v[l];
return ;//记得写完if马上敲上return,如果忘了这句return,死循环半天可能都找不出
}
Build(v, lson), Build(v, rson);//向下递归
Pushup(t);//获取到左右儿子的值后更新自身
}
void Pushdown(ll t,ll l,ll r){//懒标记下推
if(y[t]){
y[t << 1] += y[t];//把t的标记转移到左儿子上
val[t << 1] += y[t] * (mid - l + 1);//左儿子的值要加上(它区间内有多少个数 * 下放下来的标记)
y[t << 1 | 1] += y[t];//右儿子同理
val[t << 1 | 1] += y[t] * (r - mid);
y[t]=0;//记得把上方的标记清0
}
}
void Modify(ll x, ll z, ll k, ll t, ll l, ll r) {//将x到z的区间加上k
if (z < l || x > r) return ;
Pushdown(t,l,r);//不要吝啬pushdown!不知道要不要写的时候写就完了,事实上没有这句pushdown的话只能过一个点
if(z >= r && x <= l){//查到了
y[t] += k;//加上标记
val[t] += k * (r - l + 1);//加上值
return ;
}
Modify(x, z, k, lson), Modify(x, z, k, rson);//向下递归
Pushup(t);//左儿子右儿子的值更新完了之后,记得还要更新自身
}
ll Query(ll x, ll y, ll t, ll l, ll r) {//查询x到y区间的区间和
if (r < x || l > y) return 0;
Pushdown(t,l,r);//查的时候也一定要pushdown,原因在上方
if (x <= l && r <= y) return val[t];//查到了
return Query(x, y, lson) + Query(x, y, rson);//没查到,往下递归查 左儿子 和 右儿子 的和
//因为是查询,没有数值变化,所以不需要再次更新自身
}
}tree;
ll n, m;
ll v[N];//原数组
int main() {
scanf("%lld%lld", &n, &m);
for (ll i = 1; i <= n; ++i) scanf("%lld", &v[i]);//输入原数组
tree.Build(v, 1, 1, n);//初始化线段树
for (ll i = 1; i <= m; ++i) {
ll opt;
scanf("%lld", &opt);
if (opt == 1) {//将x到m区间的每个值加上k
ll x, m, k;
scanf("%lld%lld%lld",&x ,&m ,&k);
tree.Modify(x, m, k, 1, 1, n);
}
if (opt == 2) {//查询x到y的区间和
ll x, y;
scanf("%lld%lld", &x, &y);
printf("%lld\n", tree.Query(x, y, 1, 1, n));
}
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探