线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
线段树的适用范围很广,可以在线维护修改以及查询区间上的最值,求和。更可以扩充到二维线段树(矩阵树)和三维线段树(空间树)。对于一维线段树来说,每次更新以及查询的时间复杂度为O(logN)。还支持区间修改,单点修改等操作。
线段树主要是把一段大区间平均地划分成两段小区间进行维护,再用小区间的值来更新大区间。这样既能保证正确性,又能使时间保持在log级别(因为这棵线段树是平衡的)。也就是说,一个[L…R]的区间会被划分成[L…(L+R)/2]和[(L+R)/2+1…R]这两个小区间进行维护,直到L=R。但是在上述的过程中我们会遇到以下几个问题,就是我们该如何建树,建树的过程中每一个下标我们该如何去分配,分派到的每一个空间我们应该用来存放哪些数据。
在这里我们仅对线段树中常见的区间最大值问题进行解释讨论。假设所给的区间为F[1:6] = {1, 9, 7, 8, 2, 3}。那么其对应的线段树的结构就如上图所示。其中红色的圆圈就代表线段树对应的每一个结点的下标。蓝色方框中的Max就是我们每一个结点所存放的内容,即每一个区间存放的最大值。Max下面的内容是对这个区间范围的一个说明,并不需要存放在数组中。
仔细看这幅图我们会发现,其中结点的下标并不连续(在图中结点的标号并没有10,11)。这是因为我们在用数组对线段树进行模拟的时候,必须要提前对整个树的空间进行提前的开辟,所开辟的空间虽然并没有使用到,但是其仍然真是存在,这也是为什么我们在对数组进行开辟空间时一般会选择4n的大小以避免出现RE。
通过观察上面的线段树结点标号我们可以发现,对于一个区间[L,R]来说,最重要的数据当然就是区间的左右端点L和R,但是大部分的情况我们并不会去存储这两个数值,而是通过递归的传参方式进行传递。这种方式用指针好实现,定义两个左右子树递归即可,按时指针表示过于繁琐,而且不方便各种操作,大部分的线段树都是使用数组进行表示,那这里怎么快速使用下标找到左右子树呢。这就会涉及到每个结点下表数字的规律。我们发现在线段树中每个非叶子结点的度都为2,且父亲节点的左右两个孩子分别存储父亲一半的区间,而每个父亲结点存放的欧式孩子的最大值,而且左孩子的下标都为偶数,右孩子的下标都是奇数且左孩子下标数+1,即:
L = Father2 (左子树下标为父亲下标的两倍)
R = Father2+1(右子树下标为父亲下标的两倍+1)*
或
k<<1(结点k的左子树下标)
k<<1|1(结点k的右子树下标)
所以建树的操作可用如下代码实现
const int maxn = 1e5+5;
const int INF = 0x3f3f3f3f;
int tree[maxn<<2],temp[maxn];//tree[]数组表示线段树数组,temp[]表示存放原始数据的数组
void Build(int l,int r,int rt){ //l,r表示当前节点区间,rt表示当前节点编号
if(l==r) {//若到达叶节点,即区间的左右值相等
tree[rt]=temp[l];//储存数组值
return;
}
int mid = (l+r)>>1; //mid表示中间点
//左右递归
Build(l,mid,rt<<1);
Build(mid+1,r,rt<<1|1);
tree[rt] = max(tree[rt<<1],tree[rt<<1|1];//更新信息
}
假设我们将上述的区间F[1:6] = {1, 9, 7, 8, 2, 3}中的F[3] = 7通过对其+3更改为10。那么我们应当对线段树进行如下的几个操作。
//递归方式更新 updata(p,v,1,n,1);
void updata(int p,int v,int l,int r,int rt){ //p为下标,v为要加上的值,l,r为结点区间,rt为结点下标
if(l == r){ //左端点等于右端点,即为叶子结点,直接加上v即可
temp[rt] += v;
tree[rt] += v; //原数组和线段树数组都得到更新
return ;
}
int m = l + ((r-l)>>1); //m则为中间点,左儿子的结点区间为[l,m],右儿子的结点区间为[m+1,r]
if(p <= m) //如果需要更新的结点在左子树区间
updata(p,v,l,m,rt<<1);
else //如果需要更新的结点在右子树区间
updata(p,v,m+1,r,rt<<1|1);
tree[rt] = max(tree[rt<<1],tree[rt<<1|1]; //更新父节点的值
}
线段树的每个结点存储的都是一段区间的信息 ,这就意味着如果我们刚好要查询这个区间,那么则直接返回这个结点的信息即可,比如对于上面线段树,如果我直接查询[1,6]这个区间的最值,那么直接返回根节点信息10即可,查询[1,2]直接返回9。但是有时题目中为了设置难度并不会轻易让我们查询每个结点所表示的区间。比如现在我要查询[2,5]区间的最值,这时候我们会发现并不存在某个节点的区间是[2,5],那么这时我们应该采取一些什么方法来进行区间信息的查询呢?
//递归方式区间查询 query(Ld,Rd,1,n,1);
int query(int Ld,int Rd,int l,int r,int rt){ //[Ld,Rd]即为要查询的区间,l,r为结点区间,rt为结点下标
if(Ld <= l && r <= Rd) //如果当前结点的区间真包含于要查询的区间内,则返回结点信息且不需要往下递归
return tree[rt];
int ans = -INF; //返回值变量,根据具体线段树查询的什么而自定义
int mid = (l+r)>>1; //m则为中间点,左儿子的结点区间为[l,m],右儿子的结点区间为[m+1,r]
if(Ld <= m) //如果左子树和需要查询的区间交集非空
ans = max(ans, query(L,R,l,m,k<<1));
if(Rd > m) //如果右子树和需要查询的区间交集非空,注意这里不是else if,因为查询区间可能同时和左右区间都有交集
ans = max(ans, query(L,R,m+1,r,k<<1|1));
return ans; //返回当前结点得到的信息
}
}
在线段树的区间更新中我们引进了一个新的思想,Lazy_tag,字面意思就是懒惰标记的意思,实际上它的功能也就是偷懒= =,因为对于一个区间[L,R]来说,我们每次都更新区间中的每一个值,那样的话更新的复杂度将会是O(NlogN),这太高了,所以引进了Lazy_tag,这个标记一般用于处理线段树的区间更新。
线段树在进行区间更新的时候,为了提高更新的效率,所以每次更新只更新到更新区间完全覆盖线段树结点区间为止,这样就会导致被更新结点的子孙结点的区间得不到需要更新的信息,所以在被更新结点上打上一个标记,称为lazy-tag,等到下次访问这个结点的子结点时再将这个标记传递给子结点,所以也可以叫延迟标记。
也就是说递归更新的过程,更新到结点区间为需要更新的区间的真子集不再往下更新,下次若是遇到需要用这下面的结点的信息,再去更新这些结点,所以这样的话使得区间更新的操作和区间查询类似,复杂度为O(logN)。
void Pushdown(int rt){ //更新子树的lazy值,这里是RMQ的函数,要实现区间和等则需要修改函数内容
if(lazy[rt]){ //如果有lazy标记
lazy[rt<<1] += lazy[rt]; //更新左子树的lazy值
lazy[rt<<1|1] += lazy[rt]; //更新右子树的lazy值
t[rt<<1] += lazy[rt]; //左子树的最值加上lazy值
t[rt<<1|1] += lazy[rt]; //右子树的最值加上lazy值
lazy[rt] = 0; //lazy值归0
}
}
//递归更新区间 updata(L,R,v,1,n,1);
void updata(int Ld,int Rd,int v,int l,int r,int rt){ //[Ld,Rd]即为要更新的区间,l,r为结点区间,k为结点下标
if(Ld <= l && r <= Rd){ //如果当前结点的区间真包含于要更新的区间内
lazy[rt] += v; //懒惰标记
t[rt] += v; //最大值加上v之后,此区间的最大值也肯定是加v
}
else{
Pushdown(k); //重难点,查询lazy标记,更新子树
int m = l + ((r-l)>>1);
if(Ld <= m) //如果左子树和需要更新的区间交集非空
update(Ld,Rd,v,l,m,rt<<1);
if(m < Rd) //如果右子树和需要更新的区间交集非空
update(Ld,Rd,v,m+1,r,rt<<1|1);
Pushup(rt); //更新父节点
}
}
//递归方式区间查询 query(Ld,Rd,1,n,1);
int query(int Ld,int Rd,int l,int r,int rt){ //[L,R]即为要查询的区间,l,r为结点区间,k为结点下标
if(Ld <= l && r <= Rd) //如果当前结点的区间真包含于要查询的区间内,则返回结点信息且不需要往下递归
return t[rt];
else{
Pushdown(rt); /**每次都需要更新子树的Lazy标记*/
int res = -INF; //返回值变量,根据具体线段树查询的什么而自定义
int mid = l + ((r-l)>>1); //m则为中间点,左儿子的结点区间为[l,m],右儿子的结点区间为[m+1,r]
if(Ld <= m) //如果左子树和需要查询的区间交集非空
res = max(res, query(Ld,Rd,l,m,rt<<1));
if(Rd > m) //如果右子树和需要查询的区间交集非空,注意这里不是else if,因为查询区间可能同时和左右区间都有交集
res = max(res, query(Ld,Rd,m+1,r,rt<<1|1));
return res; //返回当前结点得到的信息
}
}
在沉默中爆发,在无声中绽放——xbwcj