PART 1 关于线段树
线段树是用来做一些对序列更改与查询的问题的。
它可以支持的更改有区间修改、单点修改
它可以支持的查询有区间最大/小值、区间和等等(本文将用区间和为例)
PART 2 线段树的原理
线段树就是先建一棵二叉树,每个节点保存的是要求的东西(如区间最大/小值、区间和等等)
每个叶子节点的值就是对应原序列中数的值,每个节点都有代表的一段原序列区间,当我们修改/查询时,只需要将要修改/查询的区间找出来即可。当我们一层一层回溯时,就可以通过两个儿子的值来维护当前节点的值,不需要一个一个的修改/查询,从而使时间复杂度降低至
O
(
(
N
+
M
)
l
o
g
N
)
O((N+M)log N)
O((N+M)logN)。
PART 3 建树
建树的过程就是一直递归到叶子节点,将叶子节点赋值,在回溯的同时维护当前节点的值
注:在用数组模拟二叉树时,通常用x2表示x的左儿子,用x2+1表示x的右儿子
#define ls(x) x<<1//位运算,相当于x*2
#define rs(x) x<<1|1//位运算,相当于x*2+1
void build(ll x,ll l,ll r)//x是当前节点的编号,l和r是左右区间
{
if (l==r)//如果当前左右区间相等(到达叶子节点)
{
t[x].sum=f[l];//当前节点的值就是原序列中对应位置的值
return;//返回
}
ll mid=(l+r)>>1;//位运算,相当于(l+2)/2,此处定义mid用于二分
build(ls(x),l,mid);//递归左子区间
build(rs(x),mid+1,r);//递归右子区间
update(x);
}
接下来你们应该会问update(x)是什么意思,update(x)的作用就是维护节点x的值正确
void update(ll v){t[v].sum=t[ls(v)].sum+t[rs(v)].sum;}
PART 4 区间修改
这里就要引入线段树最重要的东西——懒标记
如果我们要直接修改,每次都修改到每个与需要修改的区间有关的节点,那么每一次操作的时间复杂度可能会退化成
O
(
N
l
o
g
N
)
O(N log N)
O(NlogN)
懒标记的作用是我们对找到的区间不直接修改,而是在代表这段区间的节点打上懒标记,等到查询或修改更下层的值时才下传懒标记。
Q:既然迟早要下传懒标记,那么为什么要懒标记呢?每次修改直接修改到底不就可以了吗?
A:懒标记的作用就是延迟。如果我们先修改100000次再查询1次呢?直接修改就需要
O
(
100000
N
l
o
n
g
N
)
O(100000N long N)
O(100000NlongN)的时间复杂度,而如果用了懒标记,就可以只下传1次,时间复杂度只有
O
(
100000
l
o
g
N
)
O(100000log N)
O(100000logN)
void modify(ll x,ll l,ll r,ll a,ll b,ll c)//x,l,r所代表的和建树时一样,
//a,b,c代表a~b的区间同时加上c
{
if (a<=l && r<=b)//线段树节点代表的区间不一定刚好代表a~b的区间,
//只要当前区间被包含在a~b中,那么当前节点的值就要完全被修改
{
t[x].sum+=(r-l+1)*c;//因为当前节点所代表的是一段区间,
//而这个区间的每一个数都要被修改,所以要加上区间长度*c
t[x].tag+=c;//打上懒标记
return;
}
if (l>b || r<a) return;//很重要,如果当前区间完全与所要修改的区间无关,那么返回
//这能将每次操作的时间复杂度稳定在O(log N)
pushdown(x,l,r);//下传懒标记
ll mid=(l+r)>>1;
modify(ls(x),l,mid,a,b,c);//递归左子区间
modify(rs(x),mid+1,r,a,b,c);//递归右子区间
update(x);//维护当前节点的值
}
这里就要牵扯到一个过程——pushdown
void pushdown(ll v,ll l,ll r)//v是要下传懒标记的节点,l和r是所代表的区间
{
if (t[v].tag==0) return;//如果没有懒标记就不需要下传
ll vv=t[v].tag,vl=ls(v),vr=rs(v);//方便做题
ll mid=(l+r)>>1;
t[vl].sum+=(mid-l+1)*vv;//因为这个区间的每一个数都要被修改,
t[vr].sum+=(r-mid)*vv;//所以要加上区间长度*下传的懒标记
t[vl].tag+=vv;//儿子节点接收懒标记
t[vr].tag+=vv;
t[v].tag=0;//一定要记得清空已下传懒标记
}
PART 5 区间查询
区间查询和区间修改的思路基本一致
void query(ll x,ll l,ll r,ll x,ll y)//思路一模一样,请自行理解
{
if (x<=l && r<=y)//如果当前区间被包含在所求区间中
{
ans+=t[x].sum;//则累加当前节点的值
return;
}
if (r<x || l>y) return;
pushdown(x,l,r);
ll mid=(l+r)>>1;
query(ls(x),l,mid,x,y);
query(rs(x),mid+1,r,x,y);
}
PART 6 总结
本篇只讲述线段树的区间修改/查询,原因是如果需要单点修改/查询,只需要在修改/查询时改成
modify(1,1,n,x,x,y)
或
query(1,1,n,x,x);
即可。
希望能帮到您!
代码:(洛谷P3372【模板】线段树1)
#include<cstdio>
#define ll long long
using namespace std;
const int N=100010;
struct node
{
ll tag,l,r,sum;
}t[N<<2];
ll q,n,o,x,y,z,ans;
ll a[N];
ll ls(ll x){return x<<1;}
ll rs(ll x){return x<<1|1;}
void update(ll ln){t[ln].sum=t[ls(ln)].sum+t[rs(ln)].sum;}
void pushdown(ll ql,ll qr,ll ln)
{
if (t[ln].tag==0) return;
ll mid=(ql+qr)>>1;
t[ls(ln)].sum+=(mid-ql+1)*t[ln].tag;
t[rs(ln)].sum+=(qr-(mid+1)+1)*t[ln].tag;
t[ls(ln)].tag+=t[ln].tag;
t[rs(ln)].tag+=t[ln].tag;
t[ln].tag=0;
}
void build(ll lq,ll rq,ll now)
{
if (lq==rq)
{
t[now].l=lq;
t[now].r=rq;
t[now].sum=a[lq];
return;
}
ll mid=(lq+rq)>>1;
build(lq,mid,ls(now));
build(mid+1,rq,rs(now));
t[now].l=lq;
t[now].r=rq;
update(now);
}
void add(ll lq,ll rq,ll x,ll y,ll z,ll now)
{
if (x<=lq && rq<=y)
{
t[now].sum+=(rq-lq+1)*z;
t[now].tag+=z;
return;
}
if (rq<x || lq>y) return;
pushdown(lq,rq,now);
ll mid=(lq+rq)>>1;
add(lq,mid,x,y,z,ls(now));
add(mid+1,rq,x,y,z,rs(now));
update(now);
}
void query(ll lq,ll rq,ll x,ll y,ll now)
{
if (x<=lq && rq<=y)
{
ans+=t[now].sum;
return;
}
if (rq<x || lq>y) return;
pushdown(lq,rq,now);
ll mid=(lq+rq)>>1;
query(lq,mid,x,y,ls(now));
query(mid+1,rq,x,y,rs(now));
}
int main()
{
scanf("%lld %lld",&n,&q);
for (int i=1;i<=n;i++) scanf("%lld",&a[i]);
build(1,n,1);
for (int i=1;i<=q;i++)
{
scanf("%lld",&o);
if (o==1)
{
scanf("%lld %lld %lld",&x,&y,&z);
add(1,n,x,y,z,1);
}
if (o==2)
{
ans=0;
scanf("%lld %lld",&x,&y);
query(1,n,x,y,1);
printf("%lld\n",ans);
}
}
return 0;
}