摘自《算法竞赛进阶指南》。
线段树是一种基于分治思想的二叉树结构,用于在区间上进行信息统计。
线段树的基本特征:
1.线段树的每个节点都代表一个区间。
2.线段树具有唯一的根节点,代表的区间是整个统计范围,如[1,N]。
3.线段树的每个叶节点都代表一个长度为1的元区间[x,x]。
4.对于每个内部节点[l,r],它的左子节点是[l,mid],右子节点是[mid+1,r],其中mid=(l+r)/2(向下取整)。
线段树的节点编号方法:“父子二倍”节点编号法
1.根节点编号为1。
2.编号为x的节点的左子节点的编号为x*2,右子节点编号为x*2+1。
注意:保存线段树的数组长度要不小于4N才能保证不会越界。(N为叶节点数)
线段树的基本操作(以维护区间最大值为例)
线段树的建树
- 下面这段代码建立了一棵线段树并在每个节点上保存了对应区间的最大值。
struct SegmenTree{
int l,r;
int dat;
}t[N*4];//struct数组存储线段树
void build(int p,int l,int r){
t[p].l=l,t[p].r=r;//节点p代表区间[l,r]
if(l==r){t[p].dat=a[l];return;}//叶节点
int mid=(l+r)/2;//折半
build(p*2,l,mid);//左子节点[l,mid],编号p*2
build(p*2+1,mid+1,r);//右子节点[mid+1,r],编号p*2+1
t[p].dat=max(t[p*2].dat,t[p*2+1].dat);//从下往上传递信息
}
build(1,1,n);//调用入口
线段树的单点修改
- 把a[x]的值修改为v,时间复杂度:O(log N)。
在线段树中,根节点(编号为1的节点)是执行各种指令的入口。我们需要从根节点出发,递归找到代表区间[x,x]的叶节点,然后从下往上更新[x,x]以及它的所有祖先节点上保存的信息。
void change(int p,int x,int v){ if(t[p].l==t[p].r){t[p].dat=v;return;}//找到叶子结点 int mid=(t[p].l+t[p].r)/2; if(x<=mid)change(p*2,x,v);//x属于左半区间 else change(p*2+1,x,v);//x属于右半区间 t[p].dat=max(t[p*2].dat,t[p*2+1].dat);//从下往上更新信息 } change(1,x,v);//调用入口
线段树的区间查询
- 查询序列a在区间[l,r]上的最大值。
从根节点开始,递归执行以下过程:
1.若[l,r]完全覆盖了当前节点代表的区间,则立即回溯,并且该节点的dat值为候选答案。
2.若左子节点与[l,r]有重叠部分,则递归访问左子节点。
3.若右子节点与[l,r]有重叠部分,则递归访问右子节点。
int ask(int p,int l,int r){ if(l<=t[p].l&&r>=t[p].r)return t[p].dat;//完全包含 int mid=(t[p].l+t[p].r)/2; int val=-(1<<30);//负无穷大 if(l<=mid)val=max(val,ask(p*2,l,r));//左子节点有重叠 if(r>mid)val=max(val,ask(p*2+1,l,r));//右子节点有重叠 return val; } cout<<ask(1,l,r)<<endl;//调用入口
该查询过程会把询问区间[l,r]在线段树生分成O(log N)个节点,取它们的最大值作为答案。
原因:在每个节点[pl,p_r]上,设mid=(pl+pr)/2(向下取整),可能会出现以下几种情况:
1.l≤pl≤pr≤r,即完全覆盖了当前节点,直接返回。
2.pl≤l≤pr≤r,即只有l处于节点之中。
(1)l>mid,只会递归右子树。
(2)l≤mid,虽然递归两棵子树,但是右子节点会在递归后直接返回。
3.l≤pl≤r≤pr,即只有r处于节点之中,与情况2类似。
4.pl≤l≤r≤r,即l与r都处于节点之中。
(1)l,r都位于mid的一侧,只会递归一棵子树。
(2)l,r分别位于mid的两侧,递归左右两棵子树。
- 只有情况4(2)会真正产生对左右两颗子树的递归。这种情况至多发生一次,之后在子节点上就会变成情况2或3。因此,上述查询过程的时间复杂度为O(2logN)=O(log N)。
延迟标记(遇到区间修改时)
我们在修改指令时,可以在l≤pl≤pl≤r的情况下立即返回,只不过在回溯之前向节点p增加标记,标识“该节点曾经被修改,但其子节点尚未被更新”。
在后续的指令中,需要从节点p向下递归,我们再检查p是否具有标记。若有标记,就根据标记信息更新p的两个子节点,同时为p的两个子节点增加标记,然后清除p的标记。
这样一来,每条修改的指令的时间复杂度从O(N)降到了O(log N)。
【例题】:一个简单的整数问题
主要代码如下:
#define 100000+10
struct N SegmentTree{
int l,r;
long long sum,add;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define add(x) tree[x].add
#define sum(x) tree[x].sum
}tree[N*4];
int a[N],n,m;
void build(int p,int l,int r){
l(p)=l,r(p)=r;
if(l==r){sum(p)=a[l];return;}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
sum(p)=sum(p*2)+sum(p*2+1);
}
void spread(int p){
if(add(p)){//节点p有标记
sum(p*2)+=add(p)*(r(p*2)-l(p*2)+1);//更新左子节点信息
sum(p*2+1)+=add(p)*(r(p*2+1)-l(p*2+1)+1);//更新右子节点
add(p*2)+=add(p);//给左子节点打延迟标记
add(p*2+1)+=add(p);//给右子节点打延迟标记
add(p)=0;//清除b的标记
}
}
void change(int p,int l,int r,int d){
if(l<=l(p)&&r>=r(p)){//完全覆盖
sum(p)+=(long long)d*(r(p)-l(p)+1);//更新节点信息
add(p)+=d;//给节点打延迟标记
return;
}
spread(p);//下传延迟标记
int mid=(l(p)+r(p))/2;
if(l<=mid)change(p*2,l,r,d);
if(r>mid)change(p*2+1,l,r,d);
sum(p)=sum(p*2)+sum(p*2+1);
}
long long ask(int p,int l,int r){
if(l<=l(p)&&r>=r(p))return sum(p);
spread(p);//下传延迟标记
int mid=(l(p)+r(p))/2;
long long val=0;
if(l<=mid)val+=ask(p*2,l,r);
if(r>mid)val+=ask(p*2+1,l,r);
return val;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
build(1,1,n);
while(m--){
char op[2];int l,r,d;
cin>>op>>l>>r;
if(op[0]=='C'){
cin>>d;
change(1,l,r,d);
}
else cout<<ask(1,l,r);
}
}