[数据结构]线段树
接下来我将结合
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为
O(\log N) 。而未优化的空间复杂度为2N ,因此有时需要离散化让空间压缩。
用途
在
单点修改、区间修改、区间查询
线段树维护的信息需要满足可加性,如果使用标记,标记也要满足可加性。
实现过程
基本结构与建树
有个数组
怎么把这个数组存到线段树中呢?
-
设线段树的根节点编号为
1 -
用数组
d 来保存我们的线段树 -
如图所示:
设
-
\sf\ d[i]$ 的左节点是 $\sf\ d[ls(i)] -
\sf\ d[i]$ 的右节点是 $\sf\ d[rs(i)]
如果
\sf\ d[i] 表示的是区间\sf\ [l,r] \sf\ d[i]$ 的左儿子节点表示的是区间 $\sf\ [l, \frac{l+r}{2} ]
如何实现?
如图
#define leaf (l==r)
void build(int l,int r,int p)
{
if(leaf)
{
d[p]=a[s];
return;
}
int mid=(l+r)>>1;
build(l,mid,ls(p));
build(mid+1,r,rs(p));
d[p]=d[ls(p)]+d[rs(p)];
}
关于线段树的空间:
如果采用堆存储),则
一般来说,开四倍是怎么样都够了的,但是在卡空间的题里要进行类似于动态开点的操作。
区间查询
区间查询,比如求区间
比如查询区间
int getsum(int l,int r,int l_now,int r_now,int p)
{
//[l,r]为查询区间,[l_now,r_now]为当前节点包含的区间,p为当前节点的编号
if(l<=l_now&&r_now<=r)
{
return d[p];//当前区间为询问区间的子集时直接返回当前区间的和
}
int mid=(l_now+r_now)>>1;
int sum=0;
if(l<=mid)
{
sum+=getsum(l,r,l_now,mid,ls(p));
}
//如果左儿子代表的区间[l,m]与询问区间有交集,则递归查询左儿子
if(r>mid)
{
sum+=getsum(l,r,mid+1,r_now,rs(p));
}
//如果右儿子代表的区间[m+1,r]与询问区间有交集,则递归查询右儿子
return sum;
}
主要思路是把区间拆成左右区间,再分别处理。(分治)
区间修改&懒标记
修改区间
设一个数组
区间修改(区间加):
void interval_add(int l,int r,int val,int l_now,int r_now,int p)
{
//[l,r]为修改区间,val为被修改的元素的变化量,[l_now,r_now]为当前节点包含的区间,p为当前节点的编号
if(l<=l_now&&r_now<=r)
{
d[p]+=(r_now-l_now+1)*val;
laz[p]+=val;
return;
}//当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
int mid=(l_now+r_now)/2;
if(laz[p]&&!leaf)
{
//如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
d[ls(p)]+=laz[p]*(mid-l_now+1);
d[rs(p)]+=laz[p]*(r_now-mid);
laz[ls(p)]+=laz[p];
laz[rs(p)]+=laz[p];//将标记下传给子节点
laz[p]=0;//清空当前节点的标记
}
if(l<=mid)
{
interval_add(l,r,val,l_now,mid,ls(p));
}
if(r>mid)
{
interval_add(l,r,val,mid+1,r_now,rs(p));
}
d[p]=d[ls(p)]+d[rs(p)];
}
区间查询(求和):
int getsum(int l,int r,int l_now,int r_now,int p)
{
//[l,r]为查询区间,[l_now,r_now]为当前节点包含的区间,p为当前节点的编号
if(l<=l_now&&r_now<=r)
{
return d[p];//当前区间为询问区间的子集时直接返回当前区间的和
}
int mid=(l_now+r_now)>>1;
int sum=0;
if(l<=mid)
{
sum+=getsum(l,r,l_now,mid,ls(p));
}
//如果左儿子代表的区间[l,m]与询问区间有交集,则递归查询左儿子
if(r>mid)
{
sum+=getsum(l,r,mid+1,r_now,rs(p));
}
//如果右儿子代表的区间[m+1,r]与询问区间有交集,则递归查询右儿子
return sum;
}
如果要实现区间赋值,把所有 += 替换成 = 即可
(除
void interval_value(int l,int r,int val,int l_now,int r_now,int p)
{
if(l<=l_now&&r_now<=r)
{
d[p]=(r_now-l_now+1)*val;
laz[p]=val;
return ;
}
int mid=(l_now+r_now)>>1;
if(laz[p])
{
d[ls(p)]=laz[p]*(mid-l_now+1);
d[rs(p)]=laz[p]*(r_now-mid);
laz[ls(p)]=laz[rs(p)]=laz[p];
laz[p]=0;
}
if(l<=mid)
{
interval_value(l,r,val,l_now,mid,ls(p));
}
if(r>mid)
{
interval_value(l,r,val,mid+1,r_now,rs(p));
}
d[p]=d[ls(p)]+d[rs(p)];
}
int getsum(int l,int r,int l_now,int r_now,int p)
{
if(l<=l_now&&r_now<=r)return d[p];
int mid=(l_now+r_now)>>1;
if(laz[p])
{
d[ls(p)]=laz[p]*(mid-l_now+1);
d[rs(p)]=laz[p]*(r_now-mid);
laz[ls(p)]=laz[rs(p)]=laz[p];
laz[p]=0;
}
int sum=0;
if(l<=mid)
{
sum=getsum(l,r,l_now,mid,ls(p));
}
if(r>mid)
{
sum+=getsum(l,r,mid+1,r_now,rs(p));
}
return sum;
}
优化
-
位运算优化
-
递归到叶节点的时候叶节点一定包含在查询区间内,所以一定会在懒惰标记下放前就处理完
\sf\ return ,所以叶节点懒标记下放不会导致数组越界,也不用每次检查是否为叶节点了。 -
如果懒标记不会超出数据范围,那么可以将标记永久化,永久化可以避免下传标记,降低程序常数。
小练1.LuoGu3372:线段树1
小练2.LuoGu3373:线段树2