线段树是一种二叉搜索树,与区间树类似,它将一个区间划分红一些单元区间,每一个单元区间对应线段树中的一个叶结点。使用线段树能够快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 $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$开始递归时递归了左子树,左子树又递归左子树,一直到叶节点返回了左叶节点的值,而后和上面的同样去递归右子树,一直到叶而后返回了右叶节点的值(上面描述的可能不是太清楚,能够本身结合一下上图图),而后一层一层的返回就能够了ui
区间查询,好比求区间 $[l,r]$ 的总和(即 $a[l]+a[l+1]+ \cdots +a[r]$ )、求区间最大值/最小值……还有不少不少……怎么作呢?
3d
如上图举例
若是要查询区间 $[1,5]$ 的和,那直接获取 $d[1]$ 的值( $60$ )便可。那若是我就不查询区间 $[1,5]$ ,我就查区间 $[3,5]$ 呢?code
傻了吧。但其实呢咱们确定仍是有办法的!htm
你要查的不是 $[3,5]$ 吗?我把 $[3,5]$ 拆成 $[3,3]$ 和 $[4,5]$ 不就好了吗?blog
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 红包,两个红包的钱数相同都是 $(1000000000000001\bmod 2)$ 圆(好多啊!……不就是 $1$ 元吗……),然而由于 A 是父亲因此红包确定是先塞给 A 咯~
理论上来说 A 应该把两个红包分别给 B 和 C,可是……缺钱嘛,A 就把红包偷偷收到本身口袋里了。
A 高兴地说:「我如今有 $2$ 份红包了!我又多了 $2\times (1000000000000001\bmod 2)=2$ 元了!哈哈哈~」
可是 A 知道,若是他不把红包给 B 和 C,那 B 和 C 确定会不爽而后致使家庭矛盾最后崩溃,因此 A 对儿子 B 和 C 说:「我欠大家每人 $1$ 份 $(1000000000000001\bmod 2)$ 圆的红包,下次有新红包给过来的时候再给大家!这里我先作下记录……嗯……我钱大家各 $(1000000000000001\bmod 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\times (1000000000000001\bmod 2)$ 呢?缘由很简单:节点 A 表示的区间是 $[1,2]$ ,一共包含 $2$ 个元素。咱们是让 $[1,2]$ 这个区间的每一个元素都加上 $1000000000000001\bmod 2$ ,因此节点 A 的值就加上了 $2\times (1000000000000001\bmod 2)$ 咯。
若是这时候咱们要查询区间 $[1,1]$ (即节点 B 的值)怎么办呢?不是说了吗?若是 B 要用到的时候,A 就把它欠的还给 B!
具体是这样操做(如图):
注:为何是加上 $1\times (1000000000000001\bmod 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]; }
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; }