我们先来看两个问题:
1.给出n个数,n<=100,和m个询问,每次询问区间[l,r]的和,并输出。
确实,这个只需要用前缀和即可,复杂度O(1)
2.给出n个数,n<=1000000,和m个操作,每个操作可能有两种:1、在某个位置加上一个数;2、询问区间[l,r]的和,并输出。
如果枚举就会超时,倘若题目是修改区间【a,b】的值,则此时枚举就更不用说了,所以此时就需要用到线段树了。
先来看一下线段树的概念
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
线段树是建立在线断的基础上,每个结点都代表了一条线段[a,b]。
下图就是一棵长度范围为[1,5][1,10]的线段树。
而且,长度范围为[1,L] 的一棵线段树的深度为log (L) + 1,存储一棵线段树的空间复杂度为O(L)。
我们还可以知道线段树的两个重要性质:
1、每个节点的左孩子区间范围为[l,mid],右孩子为[mid+1,r]
2、对于结点k,左孩子结点为2*k,右孩子为2*k+1,这符合完全二叉树的性质
线段树都有以下几个操作:
1.创建线段树
2.区间查询
3.区间更新
4.单节点更新
因为线段树是一颗完全二叉树,所以我们可以用数组来建立线段树,但数组的大小需要开满二叉数的大小,我们可以知道第k层的节点数目为2的k次方,所以我们根据树深为log以2为底的n的对数,n为节点个数,由这两个可以知道数组大小应开4n。
1.线段树的建立
const maxn=50005;
int a[maxn];
int f[4*maxn];
void build(int L,int R,int id)
{
if(R==L)
f[id]=a[L]; //表示到了叶子节点
else{
int mid=(L+R)/2;
build(L,mid,id*2); //创建左子树
build(mid+1,R,id*2+1); //创建右子树
f[id]=f[id*2]+f[id*2+1]; //更新每个根节点的值为其左右子树的和
}
}
2.区间查询
(求某个区间的和)
int query(int id,int R,int L,int l,int r)
{
if(L>=l&&R<=r)
return f[id]; //已经达到了最小的区间
else{
int mid=(R+L)/2;
if(r<=mid)
return query(id*2,mid,L,l,r); //目标区间全部位于左子树
else if(l>mid)
return query(id*2+1,R,mid+1,l,r); //目标区间全部位于右子树
else //目标区间在左右子树都有
return query(id*2,mid,L,l,r)+query(id*2+1,R,mid+1,l,r);
}
}
3.单点查询
(修改数组内的一个值,例如给某个值加上某个数)
若更新叶子节点,则相应的父节点也改变,所以需要向上回溯
/*
id:当前线段树根节点的下标
b:叶子节点要加上的值
L、R:[L,R]当前节点所表示的区间
t:所要改变的节点的下标
f[]:f数组存放树
*/
void updateone(int id,int L,int R,int b,int t)
{
if(L==R)
f[id]+=b; //找到叶子节点
else{
int mid=(L+R)/2;
if(t<=mid)
updateone(id*2,L,mid,b,t); //从左子树找
else
updateone(id*2+1,mid+1,R,b,t);//从右子树找
f[id]=f[id*2]+f[id*2+1]; //更新相应根节点的值
}
}
4.区间更新
区间更新是指更新某个区间内的叶子节点的值,因为涉及到的叶子节点不止一个,而叶子节点会影响其相应的非叶父节点,那么回溯需要更新的非叶子节点也会有很多,如果一次性更新完,操作的时间复杂度肯定不是O(lgn),为此引入了线段树中的延迟标记概念。
延迟标记就是给每个做一个标记,先不往下遍历树修改节点,而是停在那个节点处等到需要对所修改的区间再进行操作时,再向下修改节点,这样修改节点和查询区间合在一起操作,就会降低复杂度。
这里以poj3468为例,具体代码见 https://blog.csdn.net/Krismile_/article/details/84305979
可以跟着代码走一遍,能更好的理解lazy标记。
下面是一些可供参考的练习题:
hdu1166 简单的单点查询、区间求和
poj2352 单点修改、区间查询
poj 3468 区间查询、区间修改
hdu 1457 求区间最大数、单点修改 (https://blog.csdn.net/Krismile_/article/details/84670894)