线段树定义
线段 = 一维数组
如图:按照下标,二分的将一个一维数组,分为两半,每个线段树节点都代表着原数组中的一段子区间,而叶子节点则代表着长度为1的区间
线段树可以用来干什么
主要是解决区间问题,比如查询一个区间的最值,给一个区间所有元素加减一个数,或者是求取某个区间的和,对于静态的区间,我们都有比较好的方法,比如【前缀和与差分数组】,【ST做RMQ静态查询】
可是这些方法都有缺点:如果我们的数组是动态更新的呢?即边更新边查询。
- 线段树可以实现动态的更新区间,同时较快的查询
指O(log(n))
线段树结构
基于上面一大堆特性,一个线段树的节点,需要有:
- 当前代表的是那个区间
- 左右孩子
- 存储的值(比如当前代表的区间的最大值)
如果用数组(即完全二叉树)来存储的话,可能还要加上
- 在数组中的下标
可以很快写出结构的定义
typedef struct node
{
int l, r, i, val;
}node;
其实都不用这么麻烦的,只用存 【val】,即该线段树节点存储的值就行了,可以用一维数组来存了
我们用数组模拟完全二叉树存放节点,但是因为二分的特性,我们不管是查找还是更新,都可以直接递推出当前节点代表的子区间,所以存储只需要存储节点的值就可以了,注意数组开大点,因为如果满打满算的话,实际节点数是 1 + 2 + 4 + 8 + ... + n/2 + n
,即满二叉树
int tree[maxn*4];
线段树创建
用数组保存节点,熟悉二叉树的话就会很快手
当前节点在数组中的下标是 i
,那么左孩子下标 2*i
, 右孩子下标 2*i+1
同样递归创建,已知原数组用 a[]
保存,trees[]
数组保存节点的值,即该节点代表的区间的最大元素的值
设当前要创建的区间是 [l, r]
,tree 数组中 i
下标代表这个区间
- 如果 l=r,那么
tree[i] = a[l]
,终止递归 - 否则,递归左右子树,即创建两个对半分的子区间,他们在tree中的下标:
2*i 与 2*i+1
,递归结束后更新 i 下标,即tree[i] = max(tree[2*i], tree[2*i+1])
/*
i : 当前线段树节点在tree数组中的下标
l : 当前线段树节点代表的区间的左端点
r : 当前线段树节点代表的区间的右端点
*/
void creat(int i, int l, int r)
{
if(l==r) {tree[i]=a[l]; return;}
creat(2*i, l, (l+r)/2), creat(2*i+1, (l+r)/2+1, r);
tree[i] = max(tree[2*i], tree[2*i+1]);
}
线段树查询
查询一个区间,因为我们二分地掌握着一些区间的值(即存在trees数组中),那么只要找到一定的区间,使得这些区间拼接起来等于【要查找的区间】,然后合并这些区间的答案即可,仍然使用递归查询,而且我们总能找到几个合适的区间,因为掌握的区间最小都是长度为1的,不行就硬凑一个出来呗
设查询的区间是 Q 我们只要找到所有 Q 的子区间即可,然后合并答案,这个过程使用递归
- 如果当前递归的节点所代表的区间,和 Q 没有交集,那么停止递归,返回最小值
- 如果当前递归的节点所代表的区间,是 Q 的子区间,那么停止递归,返回当前区间存储的值
- 否则将当前递归的节点所代表的区间对半分,递归左右子区间
/*
ql : 要查询的区间左端点
qr :要查询的区间右端点
i : 当前线段树节点在tree数组中的下标
l : 当前线段树节点代表的区间左端点
r : 当前线段树节点代表的区间右端点
*/
int query(int ql, int qr, int i, int l, int r)
{
if(qr<l || r<ql) return INT_MIN;
if(ql<=l && r<=qr) return tree[i];
return max(query(ql, qr, 2*i, l, (l+r)/2),
query(ql, qr, 2*i+1, (l+r)/2+1, r));
}
线段树区间更新
更新一个区间内的所有数字,朴素复杂度nlog(n),有点慢了,我们引入一种缓存的机制,就是把当前区间要全体增加的数字缓存起来,等我们查询的时候,可以根据这个数字来做加减
int buffer[maxn*4];
可是难题就在于如果执行多次更新操作,而且这些更新的区间各不相同,甚至还有交集,那么情况变得麻烦起来
对于更新一个区间Q,如果已知缓存了这个区间要更新的数字 v,那么这个区间最大值的结果,会加上v,我们直接修改存储在tree数组里面的值,即tree[i]+=v
,于是我们可以马上地更新到这个区间的最大值,然后把问题丢给下一层的两个子区间,即左右子区间的缓存增加了!
这样做的好处是:我们需要少量的操作次数就可以完成一个区间的修改,而剩下的操作留给查询的时候 “顺路做了”
值得一提的是
- 更新时,只有在当前区间是Q的子区间时,才能缓存要增加的值,然后把问题丢给下一层的子区间,同时更新这一层的答案,然后返回
- 如果更新时需要递归到下一层,别忘了先抛出这一层的缓存,再操作
- 递归调用更新之后,return之前记得更新这一层的值
- 查询之前,总是要记得清空当前区间的缓存,即把问题丢给下一层
/*
清空在tree数组中i下标代表的区间的缓存,向下一层抛出缓存
*/
void clear_buf(int i)
{
// 更新当前节点存储的值
tree[i] += buffer[i];
// 将缓存传递到下一层的节点
buffer[2*i] += buffer[i];
buffer[2*i+1] += buffer[i];
// 清空当前节点缓存
buffer[i] = 0;
}
/*
ul : 要更新的区间左端点
ur : 要更新的区间右端点
i : 当前区间的值存储在tree数组i下标
l : 当前区间左端点
r : 当前区间右端点
val : 要更新的值
*/
void update(int ul, int ur, int i, int l, int r, int val)
{
if(ur<l || r<ul) return;
if(ul<=l && r<=ur)
{
buffer[i] += val;
clear_buf(i);
return;
}
clear_buf(i);
update(ul, ur, 2*i, l, (l+r)/2, val);
update(ul, ur, 2*i+1, (l+r)/2+1, r, val);
tree[i] = max(tree[2*i], tree[2*i+1]);
}
同时查询函数也要记得向下抛出缓存
int query(int ql, int qr, int i, int l, int r)
{
if(qr<l || r<ql) return INT_MIN;
clear_buf(i);
if(ql<=l && r<=qr) return tree[i];
return max(query(ql, qr, 2*i, l, (l+r)/2),
query(ql, qr, 2*i+1, (l+r)/2+1, r));
}
代码
#include <bits/stdc++.h>
using namespace std;
#define maxn 1000
int tree[maxn*4];
int buffer[maxn*4];
int a[maxn], n;
void creat(int i, int l, int r)
{
if(l==r) {tree[i]=a[l]; return;}
creat(2*i, l, (l+r)/2), creat(2*i+1, (l+r)/2+1, r);
tree[i] = max(tree[2*i], tree[2*i+1]);
}
void clear_buf(int i)
{
// 更新当前节点存储的值
tree[i] += buffer[i];
// 将缓存传递到下一层的节点
buffer[2*i] += buffer[i];
buffer[2*i+1] += buffer[i];
// 清空当前节点缓存
buffer[i] = 0;
}
int query(int ql, int qr, int i, int l, int r)
{
if(qr<l || r<ql) return INT_MIN;
clear_buf(i);
if(ql<=l && r<=qr) return tree[i];
return max(query(ql, qr, 2*i, l, (l+r)/2),
query(ql, qr, 2*i+1, (l+r)/2+1, r));
}
void update(int ul, int ur, int i, int l, int r, int val)
{
if(ur<l || r<ul) return;
if(ul<=l && r<=ur)
{
buffer[i] += val;
clear_buf(i);
return;
}
clear_buf(i);
update(ul, ur, 2*i, l, (l+r)/2, val);
update(ul, ur, 2*i+1, (l+r)/2+1, r, val);
tree[i] = max(tree[2*i], tree[2*i+1]);
}
int main()
{
memset(buffer, 0, sizeof(buffer));
cin>>n;
for(int i=1; i<=n; i++) cin>>a[i];
creat(1, 1, n);
update(1, 4, 1, 1, n, 10);
update(3, 4, 1, 1, n, 10);
for(int l=1; l<=n; l++)
for(int r=l; r<=n; r++)
cout<<l<<" "<<r<<" "<<query(l, r, 1, 1, n)<<endl;
return 0;
}
/*
8
1 2 3 4 5 6 7 8
*/
8
1 2 3 4 5 6 7 8
[1, 1] 11
[1, 2] 12
[1, 3] 23
[1, 4] 24
[1, 5] 24
[1, 6] 24
[1, 7] 24
[1, 8] 24
[2, 2] 12
[2, 3] 23
[2, 4] 24
[2, 5] 24
[2, 6] 24
[2, 7] 24
[2, 8] 24
[3, 3] 23
[3, 4] 24
[3, 5] 24
[3, 6] 24
[3, 7] 24
[3, 8] 24
[4, 4] 24
[4, 5] 24
[4, 6] 24
[4, 7] 24
[4, 8] 24
[5, 5] 5
[5, 6] 6
[5, 7] 7
[5, 8] 8
[6, 6] 6
[6, 7] 7
[6, 8] 8
[7, 7] 7
[7, 8] 8
[8, 8] 8