【总结】线段树
简介
"线段树"听起来很高大上对不对?但它的本质是这样的:
你会线段,你会树,所以你会线段树。
并没有开玩笑,线段树这玩意真的不难。
按照上面那句话来看,首先我们需要线段。假设这根线段为 \([1,10]\)。
然后我们需要树。等等!线段跟树有什么关系?
见过切西瓜吧,我们不停地把西瓜从中间切开,切得更薄,直到切不动为止(起码我是这样切的,不知道为什么每次都切得很奇怪)。
线段树也是这样的,我们把这根线段不停地从中间分开,直到分不动为止。
对于一根线段,我们用 \(l\) 和 \(r\) 来描述它。就像二分一样,我们令 \(mid=(l+r)\div2\) ,那么这根线段切成的两根较短的线段就是 \([l,mid]\) 和 \([mid+1,r]\)。
以上面那根 \([1,10]\) 的线段为例,它的树就长这样:
观察上图,这棵树的最下面都是 \(l==r\) 的单个点诶!
我们发现,线段树的叶子节点个数为线段的长度。
继续观察,啊,这棵树怎么满满当当的?
我们发现,线段树除去最后一层后,是一颗满二叉树。
种一棵 线段树 的步骤:
- 有勉强还能说得过去的 二分 经验
- 准备好 l、r、mid 等工具
- 准备好 大喊"为什么我要学树状数组" 的精力
然后我们就可以开始种线段树了!
种树 建树
作为一颗优秀的二叉树,需要具备什么?
当然是左子树,右子树的编号和值!把它们存在数组里!
那么,这个存放着二叉树结点的数组需要开多大呢?也就是说,它们的编号怎么编,最大会是多少呢?
上面我们已经有了一个发现:
线段树除去最后一层后,是一颗满二叉树。
众所周知,满二叉树属于完全二叉树,所以,我们就可以按照完全二叉树的方式编号:
l=root<<1,r=l|1
// 此处l=root*2,为偶数
// 偶数|1=这个数+1
那么,最大可以编到几号呢?
一棵 \(n\) 个叶子结点的满二叉树最多有 \(n+\dfrac{n}{2}+\dfrac{n}{4}+...+2+1=2\times n-1\) 个结点,但这只是前面满二叉树的部分,再加上最后一层,编号最大可以达到大约 \(4\times n\) 。
解决了单个点,我们接着看怎么种树~
如果我们用线段树储存一个数组,那么此时线段树的左右端点就是某一段这个数组中的区间。树中的每个节点也需要有对应的值,这里我们以当前区间的最大值为例。
对于线段树中所有的叶子节点,也就是单个点,我们可以很简单的直接赋值成它的端点(左右都一样)在数组中对应的值——只有一个点,最大值肯定是自己呀!
如果不是单个点呢?我们就像切西瓜一样把它从中间切开,直到它是单个点为止咯~
如果一个区间可以继续切下去,我们就先继续切,直到只剩单个点,可以直接确定值为止。
由于这里是区间最大值,所以我们在建完左右子树后,将当前节点的值更新为左、右子树中的最大值。
Code
typedef long long ll;
const int maxn=1e5+5;
ll val[maxn];
struct segment_tree{
ll l,r;
ll max_data;
}a[maxn<<2];
ll max(ll x,ll y){
return x>y?x:y;
}
void build(ll p,ll l,ll r){
a[p].l=l,a[p].r=r;
if(l==r){
a[p].max_data=val[l];
return;
}
ll mid=l+r>>1;
build(p<<1,l,mid);
build((p<<1)|1,mid+1,r);
a[p].max_data=max(a[p<<1].max_data,a[(p<<1)|1].max_data);
return;
}
那么此时 \([1,n]\) 的最大值就是 \(a[1].max\_data\) .
咦?那么像 \([1,9]\) , \([2,8]\) 这样在树中不是完整节点的最大值呢?树状数组不是可以求任意区间嘛?
先等等,我们先把 单点修改区间查询(树状数组经典题目) 中的单点修改讲完。
单点修改,就像树状数组中的那样,修改一个点的话,就只用修改这个点和它所有的祖先的值就行了。
同样递归求解。
如果当前区间刚好就是将要更新的点(设这个点为 \(x\)),也就是说 \(l=r=x\) ,直接更新当前结点所对应的最大值。
如果 \(x\) 在当前 \([l,r]\) 的范围内,并且 \(l\ne r\) ,类似于二分地寻找 \(x\) 所对应的子树:
- \(mid=l+r>>1\)
- 如果 \(x<=mid\)
- 递归更新 \([l,mid]\)
- 否则
- 递归更新 \([mid+1,r]\)
- 更新当前结点( \(p\) )为 \(\max(a[p<<1].max\_data,a[(p<<1)+1].max\_data)\)
Code
typedef long long ll;
const int maxn=1e5+5;
struct segment_tree{
ll l,r;
ll max_data;
}a[maxn<<2];
ll max(ll x,ll y){
return x>y?x:y;
}
void Update(ll p,ll x,ll v){
if(a[p].l==a[p].r){
a[p].max_data=v;
return;
}
ll mid=a[p].l+a[p].r>>1;
if(x<=mid)Update(p<<1,x,v);
else Update((p<<1)|1,x,v);
a[p].max_data=max(a[p<<1].max_data,a[(p<<1)|1].max_data);
return;
}
最后,就可以开始区间查询了~
我们想,假设我们要查找区间 \([3,9]\) 的最大值,我们需要哪些结点的 \(max\_data\) ?
:有部分在询问区间内,继续递归询问
:完全不在询问区间内,不会被递归到
:整个在询问区间内,会被递归到,不再继续向下递归
:虽然在询问区间内,但自己的祖先也在询问区间内,不会被递归到。
我们把所有蓝色的区间放在一起,刚好就是我们的询问区间 \([3,9]\) 。
所以,大概过程就是这样的:
- 如果当前区间在询问区间内
- 返回当前结点的 \(max\_data\)
- 否则
- 如果当前节点的左子树包含一部分询问区间
- 递归询问左子树
- 如果当前结点的右子树包含一部分询问区间
- 递归询问右子树
- 返回两个询问中的最大值
- 如果当前节点的左子树包含一部分询问区间
Code
typedef long long ll;
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
struct segment_tree{
ll l,r;
ll max_data;
}a[maxn<<2];
ll max(ll x,ll y){
return x>y?x:y;
}
ll Query(ll p,ll l,ll r){
if(a[p].l>=l&&a[p].r<=r)
return a[p].max_data;
ll val=-inf;
ll mid=a[p].l+a[p].r>>1;
if(l<=mid)
val=max(val,Query(p<<1,l,r));
if(r>mid)
val=max(val,Query((p<<1)|1,l,r));
return val;
}
完整代码
#include<cstdio>
typedef long long ll;
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
ll val[maxn];
ll n,q,type,l,r,x,v;
struct segment_tree{
ll l,r;
ll max_data;
}a[maxn<<2];
void read(ll&x){
x=0;
bool f=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch&15);
ch=getchar();
}
if(f)x=-x;
return;
}
ll max(ll x,ll y){
return x>y?x:y;
}
void build(ll p,ll l,ll r){
a[p].l=l,a[p].r=r;
if(l==r){
a[p].max_data=val[l];
return;
}
ll mid=l+r>>1;
build(p<<1,l,mid);
build((p<<1)|1,mid+1,r);
a[p].max_data=max(a[p<<1].max_data,a[(p<<1)|1].max_data);
return;
}
void Update(ll p,ll x,ll v){
if(a[p].l==a[p].r){
a[p].max_data=v;
return;
}
ll mid=a[p].l+a[p].r>>1;
if(x<=mid)Update(p<<1,x,v);
else Update((p<<1)|1,x,v);
a[p].max_data=max(a[p<<1].max_data,a[(p<<1)|1].max_data);
return;
}
ll Query(ll p,ll l,ll r){
if(a[p].l>=l&&a[p].r<=r)
return a[p].max_data;
ll val=-inf;
ll mid=a[p].l+a[p].r>>1;
if(l<=mid)
val=max(val,Query(p<<1,l,r));
if(r>mid)
val=max(val,Query((p<<1)|1,l,r));
return val;
}
int main(){
read(n);
for(int i=1;i<=n;++i)
read(val[i]);
build(1,1,n);
read(q);
while(q--){
read(type);
if(type==1){
read(x);read(v);
Update(1,x,v);
}
else{
read(l);read(r);
printf("%lld\n",Query(1,l,r));
}
}
return 0;
}
例题
单点修改,区间查询 (没有找到求区间最大值,又不想塞 RMQ 的题的我)
这里要求我们求区间和,而不是最大值,但很好改,我们只需要把 max
改成 +
,把 Update
中更新值的赋值改成 +=
,把询问时求最大值返回改成求和返回就可以了。
#include<cstdio>
typedef long long ll;
const int maxn=1e6+5;
const int inf=0x3f3f3f3f;
ll val[maxn];
ll n,q,type,l,r,x,v;
struct segment_tree{
ll l,r;
ll sum;
}a[maxn<<2];
void read(ll&x){
x=0;
bool f=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch&15);
ch=getchar();
}
if(f)x=-x;
return;
}
ll max(ll x,ll y){
return x>y?x:y;
}
void build(ll p,ll l,ll r){
a[p].l=l,a[p].r=r;
if(l==r){
a[p].sum=val[l];
return;
}
ll mid=l+r>>1;
build(p<<1,l,mid);
build((p<<1)|1,mid+1,r);
a[p].sum=a[p<<1].sum+a[(p<<1)|1].sum;
return;
}
void Update(ll p,ll x,ll v){
if(a[p].l==a[p].r){
a[p].sum+=v;
return;
}
ll mid=a[p].l+a[p].r>>1;
if(x<=mid)Update(p<<1,x,v);
else Update((p<<1)|1,x,v);
a[p].sum=a[p<<1].sum+a[(p<<1)|1].sum;
return;
}
ll Query(ll p,ll l,ll r){
if(a[p].l>=l&&a[p].r<=r)
return a[p].sum;
ll val=0;
ll mid=a[p].l+a[p].r>>1;
if(l<=mid)
val+=Query(p<<1,l,r);
if(r>mid)
val+=Query((p<<1)|1,l,r);
return val;
}
int main(){
read(n);read(q);
for(int i=1;i<=n;++i)
read(val[i]);
build(1,1,n);
while(q--){
read(type);
if(type==1){
read(x);read(v);
Update(1,x,v);
}
else{
read(l);read(r);
printf("%lld\n",Query(1,l,r));
}
}
return 0;
}
更上一层楼
我们会感到疑惑:树状数组可以区间修改(虽然我直到现在都不会打),那线段树可不可以呢?
请不要小看线段树,它除了没有树状数组那么快,码量比树状数组大之外,哪儿都比树状数组好滴~
以前的区间修改,我们使用两个树状数组,一个维护差分数组,一个维护前缀和实现。
线段树自然也可以用差分,但是我们还有更魔幻的方法~
我们在每个结点中多存放一个懒惰标记(以下简称 “懒标”)。
当我们更新某个区间时(假设只更新一次),如果当前递归到的区间属于更新区间,我们就不继续更改每个点的值了,而是只让当前大区间的 \(sum\) 增加 区间长度*增加的值 。
这样,当前区间的值还是相当于整个区间的值。
但是,如果需要用到当前区间的子孙的值呢?它们并没有被更新呀?
这个时候就要用到懒标了,我们在更新时让懒标加上增加的值,也就是说,懒标存放的是这个区间帮自己的子树存的值。
在用到这个区间的子树时,我们将懒标向下拓展一层到这个区间的左子树与右子树,让左子树的 \(sum\) 存放 左子树的区间长度*懒标 ,右子树的 \(sum\) 存放 右子树的区间长度*懒标
同样,为了标记左子树与右子树帮它们的子树存放了值,我们将懒标赋给它们的懒标。
此时当前区间已经没有帮自己的子树存放值了,我们将它的懒标清空。
我们将上面的过程写在一个拓展( \(\operatorname{Spread}\) )函数中,当我们在更新或询问时,每次要向下递归时,为了保证当前节点没有帮自己的子树存放值,调用 \(\operatorname{Spread}\) 。
Code
#include<cstdio>
typedef long long ll;
const int maxn=1e6+5;
const int inf=0x3f3f3f3f;
ll val[maxn];
ll n,q,type,l,r,x;
struct _{
ll l,r;
ll sum,add;
}a[maxn<<2];
void read(ll&x){
x=0;
bool f=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch&15);
ch=getchar();
}
if(f)x=-x;
return;
}
ll max(ll x,ll y){
return x>y?x:y;
}
void Spread(ll p){
if(a[p].add){
ll lt=p<<1;
ll rt=lt|1;
a[lt].sum+=a[p].add*(a[lt].r-a[lt].l+1);
a[rt].sum+=a[p].add*(a[rt].r-a[rt].l+1);
a[lt].add+=a[p].add;
a[rt].add+=a[p].add;
a[p].add=0;
}
return;
}
void build(ll p,ll l,ll r){
a[p].l=l,a[p].r=r;
if(l==r){
a[p].sum=val[l];
return;
}
ll mid=l+r>>1;
build(p<<1,l,mid);
build((p<<1)|1,mid+1,r);
a[p].sum=a[p<<1].sum+a[(p<<1)|1].sum;
return;
}
void Update(ll p,ll x,ll y,ll v){
if(x<=a[p].l&&y>=a[p].r){
a[p].sum+=v*(a[p].r-a[p].l+1);
a[p].add+=v;
return;
}
Spread(p);
ll mid=a[p].l+a[p].r>>1;
if(x<=mid)Update(p<<1,x,y,v);
if(y>mid)Update((p<<1)|1,x,y,v);
a[p].sum=a[p<<1].sum+a[(p<<1)|1].sum;
return;
}
ll Query(ll p,ll l,ll r){
if(l<=a[p].l&&r>=a[p].r)
return a[p].sum;
ll val=0;
Spread(p);
ll mid=a[p].l+a[p].r>>1;
if(l<=mid)
val+=Query(p<<1,l,r);
if(r>mid)
val+=Query((p<<1)|1,l,r);
return val;
}
int main(){
read(n);read(q);
for(int i=1;i<=n;++i)
read(val[i]);
build(1,1,n);
while(q--){
read(type);read(l);read(r);
if(type==1){
read(x);
Update(1,l,r,x);
}
else printf("%lld\n",Query(1,l,r));
}
return 0;
}
我**我起码也码了92行你告诉我这是道 普及/提高- ??这难道不是道 省选/NOI- 吗?
拓展
我们继续。
我们在一个结点中存放下列值:
- 当前区间值的和( \(sum\) )
- 当前区间包含左端点的最大连续子段和( \(lmax\) )
- 当前区间包含右端点的最大连续子段和( \(rmax\) )
- 当前区间的最大连续子段和( \(data\) )
为了让以下代码勉强能看,我们令 \(l=\) 左子树,\(r=\) 右子树,\(p=\) 当前节点
第一行:求左子树与右子树的和,既为当前区间和
第二行:在左子树的包含左端点的最大连续子段和 和 整个左子树加上右子树包含左端点的最大连续子段和 中取最大值。
第三行:(包含右端点的最大连续子段和同理,不上图了)
第四行:在三个可能的最大连续子段和中选最大值
每次这般更新。
查询时也不能就这样:
假设需要查询 \([1,3]\)
查询到了 \([1,2].data\) 与 \([3,3].data\)
可是实际的最大连续子段和在 \([2,3].data\)
那不就尴尬了?所以这里我们不能像以前那样写。
以前:
可以查询左子树 \(\to\) 查询左子树
可以查询右子树 \(\to\) 查询右子树
现在:
不能查询左子树 \(\to\) 查询右子树
不能查询右子树 \(\to\) 查询左子树
都可以查询 \(\to\) 答案在中间,特别计算
Code
#include<cstdio>
typedef long long ll;
const int maxn=2e5+5;
struct Segment_tree{
ll l,r;
ll sum,lmax,rmax,data;
}t[maxn<<2];
ll val[maxn];
ll n,m,type,x,y;
void read(ll&x){
x=0;
bool f=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch&15);
ch=getchar();
}
if(f)x=-x;
return;
}
ll max(ll x,ll y){
return x>y?x:y;
}
void build(ll p,ll l,ll r){
t[p].l=l,t[p].r=r;
if(l==r){
t[p].data=t[p].lmax=t[p].rmax=t[p].sum=val[l];
return;
}
ll mid=l+r>>1;
ll lt=p<<1;
ll rt=lt|1;
build(lt,l,mid);
build(rt,mid+1,r);
t[p].sum=t[lt].sum+t[rt].sum;
t[p].lmax=max(t[lt].lmax,t[lt].sum+t[rt].lmax);
t[p].rmax=max(t[rt].rmax,t[rt].sum+t[lt].rmax);
t[p].data=max(t[lt].rmax+t[rt].lmax,max(t[lt].data,t[rt].data));
return;
}
void Update(ll p,ll x,ll v){
if(t[p].l==t[p].r){
t[p].lmax=t[p].rmax=t[p].data=t[p].sum=v;
return;
}
ll mid=t[p].l+t[p].r>>1;
ll lt=p<<1;
ll rt=lt|1;
if(x<=mid)Update(lt,x,v);
else Update(rt,x,v);
t[p].sum=t[lt].sum+t[rt].sum;
t[p].lmax=max(t[lt].lmax,t[lt].sum+t[rt].lmax);
t[p].rmax=max(t[rt].rmax,t[rt].sum+t[lt].rmax);
t[p].data=max(max(t[lt].data,t[rt].data),t[lt].rmax+t[rt].lmax);
return;
}
Segment_tree Query(ll p,ll lr,ll rr){
if(lr<=t[p].l&&t[p].r<=rr)
return t[p];
ll mid=t[p].l+t[p].r>>1;
ll lt=p<<1;
ll rt=lt|1;
if(rr<=mid)return Query(lt,lr,rr); //不能查右子树就查左子树
if(lr>mid)return Query(rt,lr,rr); //不能查左子树就查右子树
//没有return,两个都可以查
Segment_tree l,r,ans;
l=Query(lt,lr,mid);
r=Query(rt,mid+1,rr);
ans.sum=l.sum+r.sum;
ans.lmax=max(l.lmax,l.sum+r.lmax);
ans.rmax=max(r.rmax,r.sum+l.rmax);
ans.data=max(l.rmax+r.lmax,max(l.data,r.data));
return ans;
}
int main(){
read(n);
for(int i=1;i<=n;++i)
read(val[i]);
build(1,1,n);
read(m);
while(m--){
read(type);read(x);read(y);
if(type==1)Update(1,x,y);
else printf("%lld\n",Query(1,x,y).data);
}
return 0;
}
end.
—— · EOF · ——
真的什么也不剩啦 😖