我们先来看一个线段树长什么样子:
看到它和普通的树有什么区别的嘛,对,它胖 它的每个节点上存放着一个区间,当需要修改一个区间的值时,它能够快速的定位,当找到询问所包含的完整区间后不会再向下细分,这样子就将时间复杂度变成了log级别,我们来通过模板题看一下线段树的具体操作
题目链接
首先是建树,由于线段树的每个节点要存它的区间左右端点和一些要用的值,就不能简单只用一个数组,为了方便我们用一个struct来包含这些内容
const int N=1e5+10;
struct Node{
int L, R;//左右端点标记
LL add, sum;//sum为这个区间的和,add为懒标记,后面会说的
int len(){//省点事,输出这个区间中有多少个数
return R-L+1;
}
}tree[N*4];//一般线段树开四倍长度即可
然后是建树,与归并排序的思想有点相似
void Up(int p)//向上归并
{
tree[p].sum=tree[p<<1].sum+tree[p<<1|1].sum;
}
void build(int L, int R, int p)//建树
{
tree[p].L=L, tree[p].R=R;
if(L==R)
{
tree[p].add=0;
tree[p].sum=A[L];
return ;
}
int mid=(L+R)>>1;//先建左右子树,再归并回来
build(L, mid, p<<1);
build(mid+1, R, p<<1|1);
Up(p);
}
谈完建树就要谈线段树的操作了,线段树的操作一共有2个,修改与查询(在这题中为求和)我们先讲修改,这里就要谈到线段树修改的一个节省时间的操作了:懒标记。有时候一些修改在下面的某些查询中或者查询的正好包括这个修改的区间,不用将修改放到一个个对应的最低端的点上就能返回正确答案,这时若把这些修改全部放到最低端的点上没有任何用,反而浪费时间。我们就可以在完整包含这些点的区间上打上一个标记,等到需要下放时再将这些标记释放下去
void Down(int p){//懒标记的释放
LL &t=tree[p].add;
if(t==0) return;//没有懒标记
tree[p<<1].add+=t;
tree[p<<1].sum+=tree[p<<1].len()*t;
tree[p<<1|1].add+=t;
tree[p<<1|1].sum+=tree[p<<1|1].len()*t;
t=0;
}
void update(int L, int R, int a, int p)
{
if(tree[p].L==L&&tree[p].R==R)//若此区间刚好等于修改区间,则在此添加懒标记,否则释放此节点上的懒标记,同时向下搜寻并起来刚好等于修改区间的各个区间,还是使用了 归并的思想
{
tree[p].add+=a;
tree[p].sum+=a*(R-L+1);
return;
}
Down(p);
int mid=(tree[p].L+tree[p].R)>>1;
if(R<=mid) update(L, R, a ,p<<1);
else if(L>mid) update(L, R, a, p<<1|1);
else {
update(L, mid, a, p<<1);
update(mid+1, R, a, p<<1|1);
}
Up(p);
}
然后就是求和了,求和时跟添加时差不多,若查询的区间与当前区间有交集且交集不为此区间,则释放此区间懒标记,代码如下:
typedef long long LL;
LL query(int L, int R, int p)//题目数据较大,要用long long
{
if(tree[p].L==L&&tree[p].R==R)
{
return tree[p].sum;
}
Down(p);
int mid=(tree[p].L+tree[p].R)>>1;
if(R<=mid) return query(L, R, p<<1);
else if(L>mid) return query(L ,R, p<<1|1);
else return query(L, mid, p<<1)+query(mid+1, R, p<<1|1);
}
然后稍加润色,完整的题目代码如下
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+10;
LL A[N];
struct Node{
int L, R;
LL add, sum;
int len(){
return R-L+1;
}
}tree[N*4];
void Up(int p)
{
tree[p].sum=tree[p<<1].sum+tree[p<<1|1].sum;
}
void Down(int p){
LL &t=tree[p].add;
if(t==0) return;
tree[p<<1].add+=t;
tree[p<<1].sum+=tree[p<<1].len()*t;
tree[p<<1|1].add+=t;
tree[p<<1|1].sum+=tree[p<<1|1].len()*t;
t=0;
}
void build(int L, int R, int p)
{
tree[p].L=L, tree[p].R=R;
if(L==R)
{
tree[p].add=0;
tree[p].sum=A[L];
return ;
}
int mid=(L+R)>>1;
build(L, mid, p<<1);
build(mid+1, R, p<<1|1);
Up(p);
}
void update(int L, int R, int a, int p)
{
if(tree[p].L==L&&tree[p].R==R)
{
tree[p].add+=a;
tree[p].sum+=a*(R-L+1);
return;
}
Down(p);
int mid=(tree[p].L+tree[p].R)>>1;
if(R<=mid) update(L, R, a ,p<<1);
else if(L>mid) update(L, R, a, p<<1|1);
else {
update(L, mid, a, p<<1);
update(mid+1, R, a, p<<1|1);
}
Up(p);
}
LL query(int L, int R, int p)
{
if(tree[p].L==L&&tree[p].R==R)
{
return tree[p].sum;
}
Down(p);
int mid=(tree[p].L+tree[p].R)>>1;
if(R<=mid) return query(L, R, p<<1);
else if(L>mid) return query(L ,R, p<<1|1);
else return query(L, mid, p<<1)+query(mid+1, R, p<<1|1);
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for(int i=1; i<=n; i++)
{
scanf("%lld", &A[i]);
}
build(1, n, 1);
int c;
int R, L, X;
for(int i=0; i<m; i++)
{
scanf("%d", &c);
if(c==2)
{
scanf("%d%d", &L, &R);
printf("%lld\n", query(L, R, 1));
}
else {
scanf("%d%d%d", &L, &R, &X);
update(L, R, X, 1);
}
}
return 0;
}
其实线段树是有一定的模板在的,除了线段树节点内的内容,up()与down()函数以及query()中返回的东西要根据题目思考,其他都不需要更换。线段树的优点还是要多通过写这个来理解啊.