先给出一个很裸的线段树模板题:给出n个数,依次为a[1],a[2]…a[n],接下来有m次操作。每次让你执行两个操作:1.将区间[L,R]内的所有数+k。2.查询[L,R]范围内所有数的总和。
遇到这种题,我们就可以用线段树解决。(暴力会超时,我之前写的树状数组博客中提到过)至于线段树比较快的原因,我就不多提了
(很多博客都有提及),总之它的区间修改以及区间查询都是log级别的时间复杂度。
线段树:
- 定义:线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。 对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
- 作用 :广泛应用于区间修改和区间查询
- 结构:如下图1-1
图1-1 -
性质:
- 线段树结点标号(如下图1-2):从上至下,从左至右依次标号,例如上图中,区间[1,8]标号为1,区间[1,4]标号为2,区间[5,8]标号为3…显然,若当前结点下标为
k
,那么该结点左儿子的下标为2*k
,右儿子的下标为2*k+1
- 父亲结点与儿子节点的表示范围关系:若父亲结点表示范围为[l,r],设mid=(l+r)/2,那么左儿子的表示范围为[l,mid]、右儿子的表示范围为[mid+1,r]。
- 线段树最下面一层存储的是原数组的信息。
- 线段树数组一般开原数组的四倍大小即够用。
代码实现:
定义:
首先开一个结构体,用来存储线段树中每个结点的信息(包含的数据范围,对应的值,包括下文要提到的懒标等)
#define MAX 100010 //数据范围
int a[MAX]; //原数组
struct node
{
int l, r, k, sum, lazy;
} tr[MAX * 4]; //线段树数组
然后我们还需要一个更新函数,例如我们更改了图1-2中的结点6,那么在他之上的结点3和结点1也肯定要更改,这是必不可少的,又因为父亲结点的值=左儿子的值+右儿子的值,所以这里的更新函数就出来了
void update(int k)
{
tr[k].sum = tr[k * 2].sum + tr[k * 2 + 1].sum;
}
建树
要使用线段树肯定得先建好线段树,这里我们用递归修建线段树
void build(int k, int l, int r) //当前节点编号为k,他表示的区间[l,r]的和
{
tr[k].l = l; //先赋值区间范围
tr[k].r = r;
if (l == r) //如果这个区间是一个点,那说明已经递归到最底下一层,自然就是原数组
{
tr[k].sum = a[l];
return;
}
int mid = (tr[k].l + tr[k].r) >> 1; //将父亲区间分割成左区间和右区间
build(k * 2, l, mid); //递归建左儿子和右儿子
build(k * 2 + 1, mid + 1, r);
update(k); //递归到底层之后即可往上回溯修建各个父亲结点
}
区间修改
要修改一个区间,首先得找到一个区间。那么如何找一个区间呢?我们采取的方法是每次都从线段树顶开始找,也就是编号为1的结点,因为他肯定包含了所有区间情况。假设要修改的区间是[L,R],当前节点k代表的区间范围是[l,r],mid=(l+r)/2。每一次我们遍历一个节点的时候,将[L,R]和[l,r]比较,只会有三种情况:
- 区间[L,R]完全在节点k的左儿子包含范围内,也就是R<=mid,这时,我们只需往左递归即可。
- 区间[L,R]完全在节点k的右儿子包含范围内,也就是L>mid,同理,我们只需往右递归即可。
- 最后一种情况是难点,即L<=mid<R,这时我们要修改的区间横跨了节点k的两个儿子。这时,我们必须把区间分开处理。那应该怎么分割呢?当然是从mid处切开,将要修改的区间一分为二,变成[L,mid]和[mid+1,R],分别递归到左儿子和右儿子。
这样,如果我们要修改图1-1中的区间[5,7],那么我们最终可以找到图1-2中编号为6和14的结点。修改这两个区间就可以完成区间[5,7]的修改,问题来了,在节点6之下还有节点12,13,这两个肯定也要修改。但是如果每次区间修改都改到最底下一层的话,这样的区间修改复杂度比直接在原数组上修改还要高。如何解决呢?
懒人标记!
我们修改了节点6之后先不着急修改下面的结点12,13,(因为我们暂时用不到,没必要修改)而是在节点6上做一个标记,表示这下面需要修改,但是我们还没做!等下次要用到结点12,13了再修改
void change(int k, int l, int r, int y) //当前所在结点为k,我们要将区间[l,r]的值加上y
{
if (tr[k].l == l && tr[k].r == r) //找到了要修改的区间,标记上懒标,修改当前区间值,直接返回
{
tr[k].lazy += y;
tr[k].sum += (tr[k].r - tr[k].l + 1) * y;
return;
}
pushdown(k); //下传懒标,保证所有遍历到的结点都是更新过的值
int mid = (tr[k].l + tr[k].r) >> 1;
if (r <= mid)
change(k * 2, l, r, y);
else if (l > mid)
change(k * 2 + 1, l, r, y);
else
{
change(k * 2, l, mid, y);
change(k * 2 + 1, mid + 1, r, y);
}
update(k); //记得更新
}
void pushdown(int k) //下传懒标
{
if (tr[k].l == tr[k].r) //如果当前结点已经是最底下一层,那么直接清除懒标即可
tr[k].lazy = 0;
if (tr[k].lazy) //如果当前结点有懒人标记,那么把懒标传递给两个儿子,并且修改两个儿子的值
{
tr[k * 2].sum += tr[k].lazy * (tr[k * 2].r - tr[k * 2].l + 1);
tr[k * 2 + 1].sum += tr[k].lazy * (tr[k * 2 + 1].r - tr[k * 2 + 1].l + 1);
tr[k * 2].lazy += tr[k].lazy;
tr[k * 2 + 1].lazy += tr[k].lazy;
tr[k].lazy = 0; //传递之后记得清除当前结点的标记
}
}
区间查询
区间修改的思路理解了,区间查询也很容易理解
int query(int k, int l, int r) //查询区间[l,r]
{
if (tr[k].l == l && tr[k].r == r)
return tr[k].sum;
pushdown(k); //别忘记下传懒标,保证所有遍历到的结点都是更新过的值
int mid = (tr[k].l + tr[k].r) >> 1;
if (r <= mid)
return query(k * 2, l, r);
else if (l > mid)
return query(k * 2 + 1, l, r);
else
return (query(k * 2, l, mid) + query(k * 2 + 1, mid + 1, r));
}
以上就是线段树的模板,要想真正理解应该也要花点时间,这样做一些拓展题的时候才可以比较灵活的应用~