线段树
1 - 引入
在我们看到 修改、查询某些区间里的值 等 的题目时,最简单、最暴力的方法一定会浮现在我们的脑海当中:开数组,O(n)暴力查询修改。这样做也并不是不可以,但对于数据规模较大的题目来说,可能会收获满满De TLE。 而线段树这样一个数据结构,将每一段段的区间分散到了树上的每个节点上去了。总之,这个数据结构包含了分治和二叉树,将普通数组的O1单点、On区间的时间复杂度改为了Ologn单点区间的复杂度。
同时,线段树的父节点与子节点的关系:既然树上De每一个节点都代表着一个区间的话,父节点区间最左端就是左儿子区间的最左端,同理父节点区间最右端就是右儿子区间的最右端(如图所示),即 [FA.L, FA.R] = [LC.L, RC.R]。
【1】 [L,R] : [1, 7] |
|||||||
【2】 [L,R] : [1, 3] |
【3】 [L,R] : [4, 7] |
||||||
【4】 [L,R] : [1, 2] |
【5】 [L,R] : [3, 3] |
【6】 [L,R] : [4, 5] |
【7】 [L,R] : [6, 7] |
||||
【8】 [L,R] : [1, 1] |
【9】 [L,R] : [2, 2] |
【10】 |
【11】 - 空- |
【12】 [L,R] : [4, 4] |
【13】 [L,R] : [5, 5] |
【14】 [L,R] : [6, 6] |
【15】 [L,R] : [7, 7] |
————————————————————
2 - 原理及操作方法
————————————
维护线段树
————————————
【建树过程】原理嘞其实很简单,在引入中有说到“分治”的部分,那就用二分来做。 首先,我们拿一个节点【1】当根节点,区间大小就是[1, n];然后将这个区间分为两部分:节点【2】与节点【3】,区间大小分别为 [1, (1+n)/2] 和 [(1+n)/2+1, n]。以此类推…………。 这个时候我们可以发现节点的关系:【1】->【2】【3】,【2】->【4】【5】,【3】-> 【6】【7】……,即【n】->【n*2】【n*2+1】,同时这也是二叉树的节点性质。
由于树型结构和二分的想法,我们采用递归建树,在到达叶子节点的时候将单个点的值赋值上去。对于ta的父亲节点维护的是 某些区间的值 ,所以回溯的时候记得update一下父亲的值。
struct TREE {
int l, r;
int sum, max, min; // …………
} sm[KI];
void update(int now)
{
sm[now].sum = sm[now<<1].sum + sm[now<<1|1].sum;
sm[now].max = max(sm[now<<1].max, sm[now<<1|1].max);
sm[now].min = min(sm[now<<1].min, sm[now<<1|1].min);
}
可能一个题目不会让你只算区间和之类的,线段树除了可以维护和,你也可以对这棵树进行“改造”,比如用一下结构体,update的话可以简简单单的重载一下运算符:
struct TREE {
int l, r;
int sum, max, min; // …………
TREE(){
l = r = sum = max = min = 0;
}
};
TREE operator+(const TREE &l, const TREE &r){
TREE ans;
ans.l = l.l, ans.r = r.r;
ans.sum = l.sum + r.sum;
ans.max = max(l.max, r.max);
ans.min = min(l.min, r.min);
return ans;
}
————————————
单点查询、单点修改
————————————
让我们来举一个例子好好的研究一下线段树的操作。
请你将区间[L, R]里的某一个元素u加上x,或查询某一个元素u的值,共m次询问。
“用数组O1就可以完成的啦,用什么线段树啊” ,这时线段树与数组不同的是,Ta无法O1的找出指定元素在哪里,那么在二分查找中我们容易的想到 u 是否大于 mid,或是小于等于 mid,将复杂度压为logn,然后就可以O1愉快地修改和询问了。(code
#define ababababababa IdontKown
————————————
区间查询
————————————
虽然说在上面的例子中,由于是对单点的查询和修改,导致线段树的优势还没有发挥到完全,那就让我们再看一道题目。
请你查询区间[L, R]里的某一个区间[L',R']的值,共m次询问。
“教练,我会枚举”,往往这种题目的数据范围会比较大,至少不会让你的Onm卡过去,所以暴力枚举是不行滴,让我们拿出刚刚讲的线段树过来看看。和单点查询差不多的是,我们可以通过二分查找去查询answer。但是区间并不是一个点,除非你要找到区间就是某一个节点的范围,可以直接返回值,否则就需要分开继续向下去找,直到叶子节点,即只有一个点的时候。
``` 有一个[L --- R]的区间,你想要去查询[l --- r]区间的值。 First. 当两个区间形成这样的情况时,即要查询的区间被大的区间包含着 || ,此时我们需要继续的向下去查询。 || [L - [l -- r] - R] Second. 当两个区间形成这样的情况时,即要查询的区间包含着大的区间,此时需要先返回[L,R]的值。 || 因为我们只需对[L,R]区间进行操作,那其他的部分, || 在查询到这段区间前分配好了。 [l - [L -- R] - r] or [L/l --- R/r] Third. 当两个区间形成这样的情况时,即要查询的区间与大的区间相交织着 || ,我们的任务也只是要对[L,R]进行操作,所以去查询[l,R]和[L,r]就可以了吗? || 这是要判断[L - l - (L+R)/2 - R]的情况,也就是[l,mid]的情况,2.同理。 || [L - [l -- R] - r] or [l - [L -- r] - R] ```
(code
#define ababababababa IdontKown
————————————
区间修改、区间查询
————————————
当然一个题目或是一个实际问题,怎么可能会考你单点查询、修改,区间查询这种简单的问题呢。它既然让你改数, 肯定 不会让你只去改一个数滴,那对于一个区间该怎么修改嘞?
请你将区间[L, R]里的所有的元素u加上x,或查询[L, R]所有的元素的值,共m次询问。
"既然单点修改可以一个改,那不就是改一个区间里的所有的数吗,在选择区间查询,不就可以A了吗",
虽然说这样可以吧,有些题也确实可以通过这样暴力的修改去拿到正解(例如GSS4,虽然是暴力修改,但是也有优化),
但是你不觉得这样做太慢了吗,而且还是如此暴力的暴力。
So,当你遇到这种题目的时候,你所要在线段树中维护的区间sum,在你对区间[l,r]中的所有的数都+/-val的时候,也随之把区间[l,r]维护的sum改变了,即。等等,既然只改了这整个区间,那ta下面的小的区间你该怎么办呢?继续往下改?那这样就损失了一开始的价值了;在改的时候记录一下?对吗?OK,我们使用LAZY懒惰标记记录一下这个区间改了多少,当ta询问的时候先一步往下传LAZY标记。
#define ababababababa IdontKown
//代码是zhx讲课时的代码
//代码是zhx讲课时的代码
#define root 1,n,1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
struct node//线段树上的节点信息
{
int l,r;//这个节点左端点和右端点 所维护的区间
int sum;//当前这段区间的和
int maxv;//当前区间的最大值
int minv;//当前区间的最小值
int sum2;//|al-al+1| + |al+1 - al+2| + ... + |ar-1 - ar|
int lv,rv;//lv代表最左边的数 rv代表最右边的数
int ans;
node()
{
l=r=sum=maxv=0;
}
}z[];
node operator+(const node &l,const node &r)//l左儿子 r右儿子
{
node ans;
ans.l = l.l;
ans.r = r.r;
ans.sum = l.sum + r.sum;
ans.maxv = max(l.maxv,r.maxv);
ans.minv = min(l.minv,r.minv);
ans.sum2 = l.sum2 + r.sum2 + abs(l.rv - r.lv);
ans.lv = l.lv;
ans.rv = r.rv;
ans.ans = max( l.ans , r.ans , l.maxv - r.minv);
return ans;
}
void update(int rt)
{
z[rt] = z[rt<<1] + z[rt<<1|1];
}
void build(int l,int r,int rt)//建树 当前线段树节点编号为rt 并且当前区间为l~r
{
if (l==r)//到最底层
{
z[rt].l = z[rt].r = l;
z[rt].sum = y[l];//这段区间的和就等于第l个数
return;
}
int m=(l+r)>>1;//(l+r)/2
//左儿子所对应的区间 [l,m] 右儿子所对应的区间 [m+1,r]
build(lson);//build(l,m,rt*2)
build(rson);//build(m+1,r,rt*2+1)
update(rt);//更新rt这个节点的值
}
node query(int l,int r,int rt,int nowl,int nowr)
//当前线段树节点所对应的区间是l~r
//当前线段树节点编号是rt
//询问的区间是nowl~nowr
{
if (nowl <= l && r <= nowr) return z[rt];
int m=(l+r)>>1;
if (nowl <= m)//询问区间和左儿子有交集
{
if (m < nowr) return query(lson,nowl,nowr) + query(rson,nowl,nowr);//和右儿子有交集
else return query(lson,nowl,nowr);//只和左儿子有交集
}
else return query(rson,nowl,nowr);//只和右儿子有交集
}
build(root);
int l,r;
node ans = query(root,l,r);
————————————————————
3 - 技巧及应用、其他
3/1 - 动态开点线段树
3/2 - LAZY标记固化(永久化
【OI WIKI】如果确定懒惰标记不会在中途被加到溢出(即超过了该类型数据所能表示的最大范围),那么就可以将标记永久化。标记永久化可以避免下传懒惰标记,只需在进行询问时把标记的影响加到答案当中,从而降低程序常数。具体如何处理与题目特性相关,需结合题目来写。这也是树套树和可持久化数据结构中会用到的一种技巧。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】