线段树重温
欧耶是线段树,我们没救了。
upd.2025.7.6 小修部分冗余代码/调整部分格式。
定义
线段树是一种二叉搜索树。
线段树的每个结点都存储了一个区间,也可以理解成一个线段。
线段树就是分块思想的树化,或者说是对于信息处理的二进制化——用于达到 O(logn) 级别的处理速度,log 以 2 为底。
而分块的思想,则是可以用一句话总结为:通过将整个序列分为有穷个小块,对于要查询的一段区间,总是可以整合成 k 个所分块与 m 个单个元素的信息的并 (0≤k,m≤sqrt(n) )
用途
线段树的适用范围很广,可以在线维护修改以及查询区间上的最值,求和。实际上线段树可以处理很多符合结合律,交换律的操作。对于线段树来说,每次更新以及查询的时间复杂度为O(logN)。
1. 静态的区间询问:ST表
2. 动态的区间询问:线段树/树状数组
基础部分详解
建树
对于每个父亲管理的区间为[L, R],它左儿子管理的区间为[L, mid],右儿子管理的区间为[mid+1, R]
根据这点去递归建树即可。去遍历左子树和右子树即可。
void build(int id, int L, int R)
{
//在这里初始化懒标记,此处先不讲。
if (L==R)
{
tr[id].sum=a[L];
return ;
}
int mid=L+R>>1;
build(id*2, L, mid);
build(id*2+1, mid+1, R);
push_up(id);
}
但是这里有几个细节经常会错:
1.在L==R时,修改的是tr[id]的值而并非tr[L]的值!!注意线段树数组的下标都是以id标号的!!而L, R是对于初始化数组标号的!!!
2.只要是修改了线段树的值,都要push_up,把沿路的父亲值改掉,不要漏了!!!!!
3.线段树空间开4倍!!!
线段树开4倍空间原因:
有个公式:度为2的结点个数=度为0的结点个数-1,而线段树有个有意思的性质,没有度为1的点。所以对于线段树满足:非叶子结点个数=叶子结点个数-1
而叶子结点的个数就是数组长度,也就是n个叶子结点,n-1个非叶子结点。
把两个叶子调到最后一层,剩下n-2个叶子结点在倒数第二层。
(n-2+n-2)就是倒数第二层最右边的结点的编号,它分别有左右两个叶子。也就是最下面一层最右边的叶子可以表示为2*(2n-4)+1,编号为4n-5
所以开4n够了。
线段树构建出来具体如下图(以维护最大值为例):

单点修改+区间查询
单点修改:
跟建树差不多,不断递归定位一下这个点在哪里就好。
找到了再改。记得 push_up
区间查询:
具体看下文的区间查询,都类似就略过了,不过没有push_down操作。
板子(维护的区间加):
P3374 【模板】树状数组 1 - 洛谷 | 计算机科学教育新生态
void add(int id, int L, int R, int qL, long long k)
{
if (L==R)
{
tr[id].sum+=k;
return ;
}
int mid=L+R>>1;
if (qL<=mid) add(id*2, L, mid, qL, k);
if (qL>=mid+1) add(id*2+1, mid+1, R, qL, k);
push_up(id);
}
long long query(int id, int L, int R, int qL, int qR)
{
if (qL<=L && R<=qR) return tr[id].sum;
int mid=L+R>>1;
long long res=0;
if (qL<=mid) res+=query(id*2, L, mid, qL, qR);
if (qR>=mid+1) res+=query(id*2+1, mid+1, R, qL, qR);
return res;
}
区间修改+区间查询
这里要用懒标记了。所以在建树的时候记得初始化懒标记
区间修改:
我们规定,要修改区间为[qL, qR],线段树每个结点对应的区间为[L, R]
我们不断缩小大区间,并递归找到若干个小区间[L, R]能完全被[qL, qR]包含,并修改[L, R]的值,打上懒标记(可能你会疑惑为什么在这里打懒标记,下面有解释)就好了。
这里举个例
比如线段树维护区间[1, 6],我们要修改[2, 5]这个大区间,可以分别修改[2, 2],[3, 3],[4, 5]这几个小区间。

考虑如何算小区间[L, R]能完全被[qL, qR]包含:

画这个图理解,也就是qL<=L,且R<=qR,判断用这两个条件够了。因为只要满足这两个条件,其余条件一定满足(L<=qR,qL<=R等等)。
考虑如何去递归的找小区间:
假设当前递归到了管理区间[L, R]的结点标号id,此时需要修改[qL, qR]
分情况讨论往左子树[L, mid]或右子树[mid+1, R]再遍历。
1.往左子树[L, mid]遍历,证明[qL, qR]包含了[L, mid]的一部分,也可以说两区间有重叠部分
此时只要满足 qL<=mid 即可。这样一定包含而且不会漏其余情况。
可以这样感性理解:想要考虑两区间有一部分重叠,只需要考虑最端点的情况。分别考虑两区间的最左边和最右边端点位置情况即可。
qL,qR中最小的qL,在 L,mid中最大的mid,的左边,那不就一定重叠一部分吗。
2.往右子树遍历同理。满足qR>=mid+1即可。
管理区间[L, R]的结点标号id,要修改id的值时,无非两种情况
1.如果id是非叶子结点,修改完此时就不用继续往下找儿子了,先存懒标记,到了下次必须要利用它的儿子时候,再下放懒标记。
2.如果id是叶节点就没什么影响。因为这点没影响,所以在代码里面直接省略,没有体现。(就算被打上了懒标记并下放了也没影响)
考虑如何下放懒标记:
给当前结点的懒标记继承给它的孩子,并改变孩子的值,情况当前结点的懒标记。
在遍历的时候记得push_down,push_up(只要改了值就要push_up,只要遍历了就要push_down)
区间查询:
我们不断缩小大区间,并找到若干个小区间[L, R]能完全被[qL, qR]包含,并查询[L, R]的值,就好了。
考虑如何算小区间[L, R]能完全被[qL, qR]包含,还要递归的去找小区间,和区间修改一样。
记得push_down
板子(维护区间和):
P3372 【模板】线段树 1 - 洛谷 | 计算机科学教育新生态
void push_down(int id, int L, int R)
{
//有的题目注意,没有懒标记不能下放,此题是个例外,有没有懒标记下放都ok
int mid=L+R>>1;
tr[id*2].lazy+=tr[id].lazy;
tr[id*2+1].lazy+=tr[id].lazy;
tr[id*2].sum+=1ll*(mid-L+1)*tr[id].lazy;
tr[id*2+1].sum+=1ll*(R-(mid+1)+1)*tr[id].lazy;
tr[id].lazy=0;
}
void add(int id, int L, int R, int qL, int qR, long long k)
{
if (L>=qL && R<=qR)
{
tr[id].sum+=1ll*(R-L+1)*k;
tr[id].lazy+=k;
return ;
}
push_down(id, L, R);
int mid=L+R>>1;
if (qL<=mid) add(id*2, L, mid, qL, qR, k);
if (qR>=mid+1) add(id*2+1, mid+1, R, qL, qR, k);
push_up(id);
}
long long query(int id, int L, int R, int qL, int qR)
{
if (L>=qL && R<=qR) return tr[id].sum;
push_down(id, L, R);
int mid=L+R>>1;
long long res=0;
if (qL<=mid) res+=query(id*2, L, mid, qL, qR);
if (qR>=mid+1) res+=query(id*2+1, mid+1, R, qL, qR);
return res;
}
两个懒标记
当有多个lazy_tag时,可以考虑合并在一起,把维护多种信息看成一个整体试试。
核心思想就是把单独维护多种信息看成维护一种信息的思想
具体的,可以考虑不同信息的所有结合的顺序,然后捏出来一种大一统信息,保证这个信息在结合下是封闭的
一种常见的情况是你可以给信息钦定一个作用顺序,就比如先乘后加,再比如先推平后加之类的,然后定义反序信息作用的交换子,不过合成大信息的思考方式比较一般化
(说句闲话,这都是是粉兔大佬说的)

两个懒标记间的互相影响-结合律
P3373
考虑又要加又要乘,一个懒标记不够只能用两个。
但是乘法和加法之间运算有先后顺序(在传递懒标记的时候会有顺序),我们可以考虑消除先后顺序的影响(用结合律消除,可能吧),但是得看哪种方法更好。
记住懒标记是直接对数字操作的。
比如+ a *b +c,初值是v。
1.先加后乘
正常来说是这样:(v+a)*b+c
懒标记是这样:(v+a+c)*b
考虑消除影响变成这样:(v+a+c/b)*b
但是c/b可能会出现小数,不考虑这种方法。
2.先乘后加
正常来说是这样:(v+a)*b+c
懒标记是这样:v*b+a+c
考虑消除影响变成这样:v*b+a*b+c
这种方法一点问题没有,很好维护。
也就是说在出现一个乘法时,得把之前的加法标记先乘上这个乘法标记(因为后面传递懒标记是先乘后加),取消这个影响。
所以在区间乘操作的时候要改加法标记,取消影响。
上面只是特例分析,会特例之后考虑一般如何分析。
分析括号外面是加法还是乘法就行了,且括号里面加法乘法都要包含,这样可以探究一下对两种符号的影响。
就像这样:

反正这玩意多分析分析就出来了,并不是多么难。
区间乘法,具体如下:
void upd_mul(int id, int L, int R, int qL, int qR, long long k)
{
if (qL<=L && R<=qR)
{
tr[id].sum=(tr[id].sum*k)%Mod;
tr[id].lazy_add=(tr[id].lazy_add*k)%Mod;
tr[id].lazy_mul=(tr[id].lazy_mul*k)%Mod;
return ;
}
.....
}
传递懒标记:
还是一样,先乘后加,把父亲的乘法标记先传递过来了,再传递加法标记。
void push_down(int id, int L, int R)
{
int mid=L+R>>1;
tr[id*2].sum=(tr[id*2].sum*tr[id].lazy_mul+(mid-L+1)*tr[id].lazy_add)%Mod;
tr[id*2+1].sum=(tr[id*2+1].sum*tr[id].lazy_mul+(R-(mid+1)+1)*tr[id].lazy_add)%Mod;
tr[id*2].lazy_add=(tr[id*2].lazy_add*tr[id].lazy_mul+tr[id].lazy_add)%Mod;
tr[id*2+1].lazy_add=(tr[id*2+1].lazy_add*tr[id].lazy_mul+tr[id].lazy_add)%Mod;
tr[id*2].lazy_mul=(tr[id*2].lazy_mul*tr[id].lazy_mul)%Mod;
tr[id*2+1].lazy_mul=(tr[id*2+1].lazy_mul*tr[id].lazy_mul)%Mod;
tr[id].lazy_add=0, tr[id].lazy_mul=1;
}
区间加法,就没影响了。
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
struct node
{
long long sum, lazy_add, lazy_mul;
}tr[N<<2];
int op, n, q, x, y;
long long a[N], Mod, k;
void push_up(int id)
{
tr[id].sum=(tr[id*2].sum+tr[id*2+1].sum)%Mod;
}
void push_down(int id, int L, int R)
{
int mid=L+R>>1;
tr[id*2].sum=(tr[id*2].sum*tr[id].lazy_mul+(mid-L+1)*tr[id].lazy_add)%Mod;
tr[id*2+1].sum=(tr[id*2+1].sum*tr[id].lazy_mul+(R-(mid+1)+1)*tr[id].lazy_add)%Mod;
// printf("%d %lld %lld %lld\n", id, tr[id].lazy_mul, tr[id].lazy_add, tr[id*2+1].sum);
tr[id*2].lazy_add=(tr[id*2].lazy_add*tr[id].lazy_mul+tr[id].lazy_add)%Mod;
tr[id*2+1].lazy_add=(tr[id*2+1].lazy_add*tr[id].lazy_mul+tr[id].lazy_add)%Mod;
tr[id*2].lazy_mul=(tr[id*2].lazy_mul*tr[id].lazy_mul)%Mod;
tr[id*2+1].lazy_mul=(tr[id*2+1].lazy_mul*tr[id].lazy_mul)%Mod;
tr[id].lazy_add=0, tr[id].lazy_mul=1;
}
void build(int id, int L, int R)
{
tr[id].lazy_add=0;
tr[id].lazy_mul=1;
if (L==R)
{
tr[id].sum=a[L];
return ;
}
int mid=L+R>>1;
build(id*2, L, mid);
build(id*2+1, mid+1, R);
push_up(id);
}
void upd_mul(int id, int L, int R, int qL, int qR, long long k)
{
if (qL<=L && R<=qR)
{
tr[id].sum=(tr[id].sum*k)%Mod;
tr[id].lazy_add=(tr[id].lazy_add*k)%Mod;
tr[id].lazy_mul=(tr[id].lazy_mul*k)%Mod;
return ;
}
push_down(id, L, R);
int mid=L+R>>1;
if (qL<=mid) upd_mul(id*2, L, mid, qL, qR, k);
if (qR>=mid+1) upd_mul(id*2+1, mid+1, R, qL, qR, k);
push_up(id);
}
void upd_add(int id, int L, int R, int qL, int qR, long long k)
{
if (qL<=L && R<=qR)
{
tr[id].sum=(tr[id].sum+k*(R-L+1))%Mod;
tr[id].lazy_add=(tr[id].lazy_add+k)%Mod;
return ;
}
push_down(id, L, R);
int mid=L+R>>1;
if (qL<=mid) upd_add(id*2, L, mid, qL, qR, k);
if (qR>=mid+1) upd_add(id*2+1, mid+1, R, qL, qR, k);
push_up(id);
}
long long query(int id, int L, int R, int qL, int qR)
{
if (qL<=L && R<=qR) return tr[id].sum;
push_down(id, L, R);
int mid=L+R>>1;
long long res=0;
if (qL<=mid) res=(res+query(id*2, L, mid, qL, qR))%Mod;
if (qR>=mid+1) res=(res+query(id*2+1, mid+1, R, qL, qR))%Mod;
return res;
}
int main()
{
scanf("%d%d%lld", &n, &q, &Mod);
for (int i=1; i<=n; i++) scanf("%lld", &a[i]);
build(1, 1, n);
while (q--)
{
scanf("%d", &op);
if (op==1)
{
scanf("%d%d%lld", &x, &y, &k);
upd_mul(1, 1, n, x, y, k);
}
else if (op==2)
{
scanf("%d%d%lld", &x, &y, &k);
upd_add(1, 1, n, x, y, k);
}
else
{
scanf("%d%d", &x, &y);
printf("%lld\n", query(1, 1, n, x, y));
}
}
return 0;
}
/*
5 2 38
1 5 4 2 3
1 2 3 2
3 1 4
19
18
*/
懒标记间先后顺序的影响-交换律
P1253
区间覆盖和区间加,要考虑一个先后顺序。
两种思考方向,一种是最常见的“结合律”,两个操作也结合在一起一起下放,推平看成*0然后加一个数,就好维护了。就是维护区间乘,区间加。
还有一种,可以考虑只下放一个操作。即“交换律”
区间覆盖了,那么先前的区间加标记得清空,这样当前点只有区间覆盖标记。
区间加了,那么先前的区间覆盖操作得先下放,这样当前点只有区间加标记。
相当于交换了下执行顺序,确保每次下放标记只需要考虑下放一个标记。上面这两点是对于 下放标记,和修改,两个地方都要改的。
对于区间覆盖:
void push_down_cg(int id)
{
if (tr[id].lazy_cg!=INF)
{
tr[id*2].lazy_cg=tr[id].lazy_cg;
tr[id*2+1].lazy_cg=tr[id].lazy_cg;
tr[id*2].Max=tr[id].lazy_cg;
tr[id*2+1].Max=tr[id].lazy_cg;
tr[id*2].lazy_ad=0; //清空加法标记
tr[id*2+1].lazy_ad=0; //清空加法标记
tr[id].lazy_cg=INF;
}
}
....
void change(int id, int L, int R, int qL, int qR, int x)
{
if (qL<=L && R<=qR)
{
tr[id].Max=x;
tr[id].lazy_cg=x;
tr[id].lazy_ad=0; //清空加法标记
return ;
}
push_down(id);
int mid=L+R>>1;
if (qL<=mid) change(id*2, L, mid, qL, qR, x);
if (qR>=mid+1) change(id*2+1, mid+1, R, qL, qR, x);
push_up(id);
}
对于区间加:
void push_down_ad(int id)
{
if (tr[id].lazy_ad!=0)
{
push_down_cg(id); //提前下放覆盖标记
tr[id*2].lazy_ad+=tr[id].lazy_ad;
tr[id*2+1].lazy_ad+=tr[id].lazy_ad;
tr[id*2].Max+=tr[id].lazy_ad;
tr[id*2+1].Max+=tr[id].lazy_ad;
tr[id].lazy_ad=0;
}
}
...
void Add(int id, int L, int R, int qL, int qR, int x)
{
if (qL<=L && R<=qR)
{
push_down_cg(id); //提前下放覆盖标记
tr[id].Max+=x;
tr[id].lazy_ad+=x;
return ;
}
push_down(id);
int mid=L+R>>1;
if (qL<=mid) Add(id*2, L, mid, qL, qR, x);
if (qR>=mid+1) Add(id*2+1, mid+1, R, qL, qR, x);
push_up(id);
}
左右前缀区间维护段树
大概是维护一段连续区间的某些值要用到吧。
主要就分情况讨论,如下:


板子[无区查]
P6492 [COCI 2010/2011 #6] STEP - 洛谷 | 计算机科学教育新生态
记录 tr[i].ans,表示当前区间的最优答案,这个答案,可以考虑由两个孩子的 ans 得来,也可以考虑是两个孩子拼接后的中间那一部分。
所以再维护 tr[i].ansL,表示当前区间的前缀最优答案,tr[i].ansR,就是后缀。
就可以通过 tr[i].ansL,tr[i].ansR,转移到 tr[i].ans 了。
再如何维护tr[i].ansL,tr[i].ansR?
直接继承儿子的就行。注意分类讨论一组特殊情况。
如果儿子的 tr[i].ansL,即前缀最优答案,是这个儿子的整个区间,那要再加上这个另外一个儿子的 ans[i].ansL,因为可以拼接而成。
tr[i].ansR 维护同理
注意,每题的 push_up 维护方法不一定固定。但大致思路一样。
比如下面那道板题,维护 tr[i].ansL/R时,必须不能加判断当前前缀/后缀的最优答案是否为整个区间。但是这题必须要判断。
具体一点,就看题解P6492 [COCI2010-2011#6] STEP - 洛谷专栏。很详细了。
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
struct node
{
int ans, ansL, ansR;
}tr[N<<2];
int n, q, a[N], x;
void push_up(int id, int L, int R)
{
int mid=L+R>>1;
tr[id].ans=max(tr[id*2].ans, tr[id*2+1].ans);
if (a[mid]!=a[mid+1]) tr[id].ans=max(tr[id].ans, tr[id*2].ansR+tr[id*2+1].ansL);
tr[id].ansL=tr[id*2].ansL;
if (tr[id*2].ans==mid-L+1 && a[mid]!=a[mid+1]) tr[id].ansL=max(tr[id].ansL, mid-L+1+tr[id*2+1].ansL);
tr[id].ansR=tr[id*2+1].ansR;
if (tr[id*2+1].ans==R-(mid+1)+1 && a[mid]!=a[mid+1]) tr[id].ansR=max(tr[id].ansR, R-(mid+1)+1+tr[id*2].ansR);
}
void build(int id, int L, int R)
{
tr[id].ans=tr[id].ansL=tr[id].ansR=1;
if (L==R)
{
return ;
}
int mid=L+R>>1;
build(id*2, L, mid);
build(id*2+1, mid+1, R);
push_up(id, L, R);
}
void change(int id, int L, int R, int x)
{
if (L==R)
{
a[L]^=1;
return ;
}
int mid=L+R>>1;
if (x<=mid) change(id*2, L, mid, x);
if (x>=mid+1) change(id*2+1, mid+1, R, x);
push_up(id, L, R);
}
int query()
{
return tr[1].ans;
}
int main()
{
scanf("%d%d", &n, &q);
for (int i=1; i<=n; i++) a[i]=1;
build(1, 1, n);
while (q--)
{
scanf("%d", &x);
change(1, 1, n, x);
printf("%d\n", query());
}
return 0;
}
/*
3 1
2
*/
板子[带区查]
其实带区查差不多,就是细节多了点。
区间查询的时候跟push_up一样,分情况讨论,合并区间答案即可。可以直接套用push_up的代码。
在不断递归下去的时候,具体这样分情况(可以看代码理解。有点抽象)
1)查询区间只在左区间,直接查询左区间
2)查询区间只在右区间,直接查询右区间
3)查询区间左右区间都有,考虑如何合并
因为有合并区间的情况,所以 query 返回的应该是一个结构体,且维护方式和 push_up 一样。
对于这题,维护区间连续段和的最大值即可。然后再开区间前缀和最大值,后缀和最大值辅助更新。
维护 tr[i].ans 时,注意跨越两段的情况,这个时候两段可能有负数,所以可以只选1段,就直接考虑最大值。或者选2段,直接相加。
维护 tr[i].ansL/R 时,注意不能判断当前前缀/后缀的最优答案是否为整个区间,因为全选当前区间可能不优,但是另一半边的区间选上后可能更优。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5, INF=-1e18;
struct node
{
int sum;
int Max, MaxL, MaxR;
}tr[N<<2];
int n, m, a[N], op, x, y;
void push_up(int id, int L, int R)
{
int mid=L+R>>1;
tr[id].sum=tr[id*2].sum+tr[id*2+1].sum;
tr[id].Max=max(tr[id*2].Max, tr[id*2+1].Max);
tr[id].Max=max(tr[id].Max, tr[id*2+1].MaxL+tr[id*2].MaxR);
tr[id].MaxL=tr[id*2].MaxL;
tr[id].MaxL=max(tr[id].MaxL, tr[id*2].sum+tr[id*2+1].MaxL);
tr[id].MaxR=tr[id*2+1].MaxR;
tr[id].MaxR=max(tr[id].MaxR, tr[id*2+1].sum+tr[id*2].MaxR);
}
void build(int id, int L, int R)
{
tr[id].sum=0;
tr[id].Max=tr[id].MaxL=tr[id].MaxR=INF;
if (L==R)
{
tr[id].sum=tr[id].Max=tr[id].MaxL=tr[id].MaxR=a[L];
return ;
}
int mid=L+R>>1;
build(id*2, L, mid);
build(id*2+1, mid+1, R);
push_up(id, L, R);
}
node query(int id, int L, int R, int qL, int qR)
{
if (qL<=L && R<=qR) return tr[id];
int mid=L+R>>1;
if (qR<=mid) return query(id*2, L, mid, qL, qR); //left
else if (qL>=mid+1) return query(id*2+1, mid+1, R, qL, qR); //right
else //all
{
node res, resL, resR;
resL=query(id*2, L, mid, qL, qR), resR=query(id*2+1, mid+1, R, qL, qR);
res.sum=resL.sum+resR.sum;
res.Max=max(resL.Max, resR.Max);
res.Max=max(res.Max, max(max(resL.MaxR, resR.MaxL), resL.MaxR+resR.MaxL));
res.MaxL=resL.MaxL;
res.MaxL=max(res.MaxL, resL.sum+resR.MaxL);
res.MaxR=resR.MaxR;
res.MaxR=max(res.MaxR, resR.sum+resL.MaxR);
return res;
}
}
void change(int id, int L, int R, int p, int s)
{
if (L==R)
{
tr[id].Max=tr[id].MaxL=tr[id].MaxR=s;
tr[id].sum=s;
return ;
}
int mid=L+R>>1;
if (p<=mid) change(id*2, L, mid, p, s);
if (p>=mid+1) change(id*2+1, mid+1, R, p, s);
push_up(id, L, R);
}
signed main()
{
scanf("%d%d", &n, &m);
for (int i=1; i<=n; i++) scanf("%lld", &a[i]);
build(1, 1, n);
while (m--)
{
scanf("%d%d%lld", &op, &x, &y);
if (op==1)
{
if (x>y) swap(x, y);
printf("%lld\n", query(1, 1, n, x, y).Max);
}
else change(1, 1, n, x, y);
}
return 0;
}
势能线段树
板子[单点势能]
P4145
作为一个好用的黑科技,还是多讲下吧。
势能线段树的介绍:
本质上是一种区间暴力修改技术。
考虑普通的区修线段树,我们使用 lazytag 来记录区间修改信息,在访问到当前节点之后再将当前节点的 tag 下传。
然而,有很多信息是不好下传的(不满足交换或结合律),于是我们就无法使用 tag 来记录区修信息。
但有的题目每一个操作都有潜在的操作次数上限,我们可以先判断该区间有没有需要操作的节点,有的话遍历到叶子节点进行修改,没有的话返回。
相对应的,和势能线段树配合着用的一个trict:
如果要看某个区间的每个值是否>x,只需要找区间的最小值。最大值同理
比如这题就需要,常用于次数限制。
此题同理,每次都sqrt,从1e12变成1最多只需7次。变成1之后,再sqrt没有意义,怎么样都是1。
所以可以维护一个区间最大值,看看最大值是否>1,>1才进行递归并修改。
修改操作可以暴力一点,不用懒标记了,直接递归到叶子结点进行修改(因为不符合结合律交换律)
区间势能
2.9.数据结构PPT
咕咕咕。
例题
其他技巧
P1558-线段树+状压
关注到数据范围较小,且正常维护不好维护。考虑维护一个二进制数,表状态压缩,就好了。
P1438-线段树维护差分数组
区间加等差数列,因为是等差,所以中间一串的差相等。考虑维护中间一段,两边特殊处理就好。所以可以想到维护差分数组。
具体的:
原序列:0 0 0 0 0 0 差分序列:0 0 0 0 0 0 等差序列:1 3 5 7 9 加上等差数列后的序列:1 3 5 7 9 0 然后差分:1 2 2 2 2 -9
当前也可以考虑另一种思路,因为是单点查询,所以有这种思路:题解 P1438 【无聊的数列】 - 洛谷专栏
当我们对区间 $[l,r]$ 加上一个首项为 $k$ 公差为 $d$ 的等差数列时,我们给这个区间加上 $k - d \times l$,再给每个位置加上 下标$\times d$
P3870-区间01串取反trict
维护区间1的个数,每次会对区间取反。因为只有0/1,所以取反后,1的个数=区间长度-原来1的个数。
根据值域建树,解决一类偏序问题
可以解决 I<j, a[i]<a[j] 的数对个数,直接按照值域建线段树就好。
1.P1908 逆序对 - 洛谷,离散化后,容斥一下,就是板子了。
此题同理,三元组只需以中间一元为中心考虑问题即可。即枚举中间一元,快速维护左右两元非法数量。

浙公网安备 33010602011771号