什么是线段树
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 (O(log N)) 。而未优化的空间复杂度为 (2N) ,因此有时需要离散化让空间压缩。——by 百度
怎么构造线段树
首先明确一件事,根((root))的左孩子是$root cdot 2 (或)root << 1(,右孩子是)root cdot 2+1(或)root<< 1 | 1$
我们有个大小为 (5) 的数组 (a={10,11,12,13,14}) 要进行区间求和操作,现在我们要怎么把这个数组存到线段树中(也可以说是转化成线段树)呢?我们这样子做:设线段树的根节点编号为 (1) ,用数组 (d) 来保存我们的线段树, (d[i]) 用来保存编号为 (i) 的节点的值(这里节点的值就是这个节点所表示的区间总和),如图所示:
上图来自(oi-wiki)
void build(int root,int l,int r) {
if(l==r) {//如果到了叶子节点就直接赋值
tree[root]=a[l];
return;
}
int mid=(l+r)/2;
build(root*2,l,mid);//递归左子树
build(root*2+1,mid+1,r);//递归右子树
tree[root]=tree[root*2]+tree[root*2+1];//注意需要更新根节点
}
我们来看一下(build)函数的运行过程,当从(root)开始递归时递归了左子树,左子树又递归左子树,一直到叶节点返回了左叶节点的值,然后和上面的一样去递归右子树,一直到叶然后返回了右叶节点的值(上面描述的可能不是太清楚,可以自己结合一下上图图),然后一层一层的返回就可以了
线段树的区间查询
区间查询,比如求区间 ([l,r]) 的总和(即 (a[l]+a[l+1]+ cdots +a[r]) )、求区间最大值/最小值……还有很多很多……怎么做呢?
如上图举例
如果要查询区间 ([1,5]) 的和,那直接获取 (d[1]) 的值( (60) )即可。那如果我就不查询区间 ([1,5]) ,我就查区间 ([3,5]) 呢?
傻了吧。但其实呢我们肯定还是有办法的!
你要查的不是 ([3,5]) 吗?我把 ([3,5]) 拆成 ([3,3]) 和 ([4,5]) 不就行了吗?
int query(int root,int l,int r,int x,int y) {
if(x<=l && r<=y) return tree[root];
int mid=(l+r)/2;
int ans=0;
pushdown(root,l,r,mid);
if(x<=mid) ans+=query(root*2,l,mid,x,y);
if(mid<y) ans+=query(root*2+1,mid+1,r,x,y);
return ans;
}
线段树的区间修改与懒惰标记
这里就是线段树的精髓了,请仔细理解
区间修改是个很有趣的东西……你想啊,如果你要修改区间 ([l,r]) ,难道把所有包含在区间[l,r]中的节点都遍历一次、修改一次?那估计这时间复杂度估计会上天。这怎么办呢?我们这里要引用一个叫做 「懒惰标记」 的东西。
我们设一个数组 (b) , (b[i]) 表示编号为 (i) 的节点的懒惰标记值。啥是懒惰标记、懒惰标记值呢?这里我再举个例子:
A 有两个儿子,一个是 B,一个是 C。
有一天 A 要建一个新房子,没钱。刚好过年嘛,有人要给 B 和 C 红包,两个红包的钱数相同都是 ((1000000000000001mod 2)) 圆(好多啊!……不就是 (1) 元吗……),然而因为 A 是父亲所以红包肯定是先塞给 A 咯~
理论上来讲 A 应该把两个红包分别给 B 和 C,但是……缺钱嘛,A 就把红包偷偷收到自己口袋里了。
A 高兴地说:「我现在有 (2) 份红包了!我又多了 (2 imes (1000000000000001mod 2)=2) 元了!哈哈哈~」
但是 A 知道,如果他不把红包给 B 和 C,那 B 和 C 肯定会不爽然后导致家庭矛盾最后崩溃,所以 A 对儿子 B 和 C 说:「我欠你们每人 (1) 份 ((1000000000000001mod 2)) 圆的红包,下次有新红包给过来的时候再给你们!这里我先做下记录……嗯……我钱你们各 ((1000000000000001mod 2)) 圆……」
儿子 B、C 有点恼怒:「可是如果有同学问起我们我们收到了多少红包咋办?你把我们的红包都收了,我们还怎么装X?」
父亲 A 赶忙说:「有同学问起来我就会给你们的!我欠条都写好了不会不算话的!」
这样 B、C 才放了心。
在这个故事中我们不难看出,A 就是父亲节点,B 和 C 是 A 的儿子节点,而且 B 和 C 是叶子节点,分别对应一个数组中的值(就是之前讲的数组 (a) ),我们假设节点 A 表示区间 ([1,2]) (即 (a[1]+a[2]) ),节点 B 表示区间 ([1,1]) (即 (a[1]) ),节点 C 表示区间 ([2,2]) (即 (a[2]) ),它们的初始值都为 (0) (现在才刚开始呢,还没拿到红包,所以都没钱~)。
如图:
注:这里 D 表示当前节点的值(即所表示区间的区间和)。
为什么节点 A 的 D 是 (2 imes (1000000000000001mod 2)) 呢?原因很简单:节点 A 表示的区间是 ([1,2]) ,一共包含 (2) 个元素。我们是让 ([1,2]) 这个区间的每个元素都加上 (1000000000000001mod 2) ,所以节点 A 的值就加上了 (2 imes (1000000000000001mod 2)) 咯。
如果这时候我们要查询区间 ([1,1]) (即节点 B 的值)怎么办呢?不是说了吗?如果 B 要用到的时候,A 就把它欠的还给 B!
具体是这样操作(如图):
注:为什么是加上 (1 imes (1000000000000001mod 2)) 呢?
原因和上面一样——B 和 C 表示的区间中只有 (1) 个元素啊!
由此我们可以得到,区间 ([1,1]) 的区间和就是 (1) 啦!O(∩_∩)O 哈哈~!
PS:上述解释来自(Oi-wiki),我觉得解释的很好可以看看,附上上面解释的原版代码
void update(int l, int r, int c, int s, int t,int p){
// [l,r] 为修改区间,c 为被修改的元素的变化量,[s,t] 为当前节点包含的区间,p 为当前节点的编号
if (l <= s && t <= r) {
d[p] += (t - s + 1) * c, b[p] += c;
return;
}// 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
int m = (s + t) / 2;
if (b[p] && s!=t){
// 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
b[p] = 0; // 清空当前节点的标记
}
if (l <= m) update(l, r, c, s, m, p * 2);
if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
d[p] = d[p * 2] + d[p * 2 + 1];
}
下面是我的代码:
区间修改
void update(int root,int l,int r,int x,int y,int v) {
if(x<=l && r<=y) return add(root,l,r,v);
int mid=(l+r)/2;
pushdown(root,l,r,mid);
if(x<=mid) update(root*2,l,mid,x,y,v);
if(y>mid) update(root*2+1,mid+1,r,x,y,v);
tree[root]=tree[root*2]+tree[root*2+1];
}
(add)函数
void add(int root,int l,int r,int v) {
tree[root]+=v*(r-l+1);
lazy[root]+=v;
}
下放懒标记
void pushdown(int root,int l,int r,int mid) {
if(lazy[root]==0) return ;
add(root*2,l,mid,lazy[root]);
add(root*2+1,mid+1,r,lazy[root]);
lazy[root]=0;
}
单点修改
单节点更新是指只更新线段树的某个叶子节点的值,但是更新叶子节点会对其父节点的值产生影响,因此更新子节点后,要回溯更新其父节点的值。
/*
功能:更新线段树中某个叶子节点的值
root:当前线段树的根节点下标
[nstart, nend]: 当前节点所表示的区间
index: 待更新节点在原始数组arr中的下标
addVal: 更新的值(原来的值加上addVal)
*/
void updateOne(int root, int nstart, int nend, int index, int addVal)
{
if(nstart == nend)
{
if(index == nstart)//找到了相应的节点,更新之
segTree[root].val += addVal;
return;
}
int mid = (nstart + nend) / 2;
if(index <= mid)//在左子树中更新
updateOne(root*2+1, nstart, mid, index, addVal);
else updateOne(root*2+2, mid+1, nend, index, addVal);//在右子树中更新
//根据左右子树的值回溯更新当前节点的值
segTree[root].val = segTree[root*2+1].val+segTree[root*2+2].val;
}
参考资料
1.oi-wiki
3.信息学奥赛一本通