一、线段树的概论
二、线段树的性质
三、线段树的建树
四、线段树的单点修改
五、线段树的区间查询
六、线段树的区间修改
七、代码实现
假设有编号从1到n的n个点,每个点都存了一些信息,用[L,R]表示下标从L到R的这些点。
线段树的用处就是,对编号连续的一些点进行修改或者统计操作,修改和统计的复杂度都是O(log2(n)).
线段树的原理,就是,将[1,n]分解成若干特定的子区间,然后,将每个区间[L,R]都分解为少量特定的子区间,通过对这些少量子区间的修改或者统计,来实现快速对[L,R]的修改或者统计。
由此看出,用线段树统计的东西,必须符合区间加法,否则不可能通过分成的子区间来得到[L,R]的统计结果。
符合区间加法的例子:
数字之和——总数字之和 = 左区间数字之和 + 右区间数字之和
最大值——总最大值=max(左区间最大值,右区间最大值)
不符合区间加法的例子:
众数——只知道左右区间的众数,没法求总区间的众数
如果观察一下线段树(无奈这里木有图),会发现除去线段树的最后一层,整颗线段树是一颗完全二叉树,树深为O(logN),因此我们可以用“父子二倍”标号方法:
1.根节点编号为1
2.编号为x的节点的左子节点编号为x*2,右子节点编号为x*2+1
线段树的基本用途是对序列进行维护,支持查询与修改指令。给定一个长度为N的序列A,我么可以在序列[1,N]上建立一颗线段树,每个叶节点[i,]保存A[i]的值。线段树的二叉树结构可以很方便地从下往上传递信息。以区间最大值为例,d(l,r)等于max{A[i]},显然d(l,mid)=max(d(l,mid),d(mid+1,r))
下面的代码建立了线段树并保留了最大值:
单点修改是一条形如“C x v"的指令,表示把A[x]的值修改为v
在线段树中,根节点(编号为1的节点)是执行各种指令的入口,我们得从根节点入手,递归找到[x,x]叶节点,然后从下往上更新[x,x],以及所有祖先节点上保留的信息
时间复杂度为O(logN)
区间查询是一条形如”Q l r"的指令,例如查询序列A在区间[l,r]上的最大值,即max{A[i]}.我们只需要从根节点开始
1.若“l,r"完全覆盖了当前节点代表的空间,则立即回溯,并且将该节点的值为候选答案
2.若左子节点与[l,r]有重叠部分,则递归左子节点
3.若右子节点与[l,r]有重叠部分,则递归右子节点
线段树的区间修改也是将区间分成子区间,但是要加一个标记,称作懒惰标记(lazy_tag)。
标记的含义:本节点的统计信息已经根据标记更新过了,但是本节点的子节点仍需要进行更新。
即,如果要给一个区间的所有值都加上k,那么,实际上并没有给这个区间的所有值都加上k,而是打个标记,记下来,这个节点所包含的区间需要加k.打上标记后,要根据标记更新本节点的统计信息,比如,如果本节点维护的是区间和,而本节点包含5个数,那么,打上+k的标记之后,要给本节点维护的和+5k。
(1)定义
#define N 100005
int A[N],n,N;//原数组
int Sum[N<<2];
int Add[N<<2];//懒惰标记
(2)建树
void PushUp(int rt){Sum[rt]=Sum[rt<<1]+Sum[rt<<1|1];}
void Build(int l,int r,int rt)
{ //l,r表示当前节点区间,rt表示当前节点编号
if(l==r)
{//若到达叶节点
Sum[rt]=A[l];//储存数组值
return;
}
int m=(l+r)>>1;
//左右递归
Build(l,m,rt<<1);
Build(m+1,r,rt<<1|1);
//更新
PushUp(rt);
}
(3)点修改
void Update(int L,int C,int l,int r,int rt)
{//l,r表示当前节点区间,rt表示当前节点编号
if(l==r)
{//到叶节点,修改即可
Sum[rt]+=C;
return;
}
int m=(l+r)>>1;
//根据条件判断往左子树调用还是往右
if(L <= m) Update(L,C,l,m,rt<<1);
else Update(L,C,m+1,r,rt<<1|1);
PushUp(rt);//子节点更新了,本节点也需要更新信息
}
(4)下推标记
void PushDown(int rt,int ln,int rn)
{
//ln,rn为左子树,右子树的数字数量。
if(Add[rt])
{
//下推标记
Add[rt<<1]+=Add[rt];
Add[rt<<1|1]+=Add[rt];
//修改子节点的Sum使之与对应的Add相对应
Sum[rt<<1]+=Add[rt]*ln;
Sum[rt<<1|1]+=Add[rt]*rn;
//清除标记
Add[rt]=0;
}
}
(5)区间修改
void Update(int L,int R,int C,int l,int r,int rt)
{//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
if(L <= l && r <= R)
{//如果本区间完全在操作区间[L,R]以内
Sum[rt]+=C*(r-l+1);//更新
Add[rt]+=C;//增加懒惰标记,表示本区间的Sum正确,子区间的Sum仍需要根据懒惰的值来调整
return;
}
int m=(l+r)>>1;
PushDown(rt,m-l+1,r-m);//下推标记
//判断左右子树跟[L,R]有无交集
if(L <= m) Update(L,R,C,l,m,rt<<1);
if(R > m) Update(L,R,C,m+1,r,rt<<1|1);
PushUp(rt);//更新
}
(6)区间查询
int Query(int L,int R,int l,int r,int rt)
{//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
if(L <= l && r <= R)
{
//在区间内
return Sum[rt];
}
int m=(l+r)>>1;
//下推标记
PushDown(rt,m-l+1,r-m);
int ANS=0;
if(L <= m) ANS+=Query(L,R,l,m,rt<<1);
if(R > m) ANS+=Query(L,R,m+1,r,rt<<1|1);
return ANS;
}
(7)函数调用
//建树
Build(1,n,1);
//点修改
Update(L,C,1,n,1);
//区间修改
Update(L,R,C,1,n,1);
//区间查询
int ANS=Query(L,R,1,n,1);