1.定义
线段树是一种二叉搜索树
线段树将一段区间[a,b]划分为一些单位区间,每个单位区间对应线段树的一个叶子节点(元节点)。
对于线段树中的每一个非叶子节点[a,b],它的左儿子区间[a,(a+b)/2],右儿子区间[(a+b)/2+1,b]
给张图直观理解下
用线段树统计的东西必须符合区间加法,即[a,b]区间的结果可以由[a,(a+b)/2]和[(a+b)/2+1,b]相“加”获得。
符合区间加法的例子有:
数字之和——总数字之和 = 左区间数字之和 + 右区间数字之和
最大公因数(GCD)——总GCD = gcd( 左区间GCD , 右区间GCD );
最大值——总最大值=max(左区间最大值,右区间最大值)
不符合区间加法的例子:
众数——只知道左右区间的众数,没法求总区间的众数
01序列的最长连续零——只知道左右区间的最长连续零,没法知道总的最长连续零
线段树分解:递归的将区间[l,r],分割为[l,m]和[m+1,l],其中m=(l+r)/2,假设根的高度为1,树的高度为 l o g 2 ( n − 1 ) + 2 ( n > 1 ) log_2(n-1)+2(n>1) log2(n−1)+2(n>1),对于每个n树的分解是唯一的,所以n相同,树结构相同,为可持久化线段树提供了基础。
理论
- 1.点修改:线段树修改某一点,只需要修改每一层的一个点,修改的次数最大值等于树的高度
- 2.区间查询:n>=3时,一个[1,n]的线段树可以将[1,n]的任意子区间[L,R]分解为不超过 2 l o g 2 ( n − 1 ) 2log_2(n-1) 2log2(n−1)个子区间,对该结论证明感兴趣的可以参考本文最后的引用处。
- 3.线段树的区间修改:线段树的区间修改也是将区间分成子区间,但是要加一个标记,称作懒惰标记。标记的含义:本节点的统计信息已经根据标记更新过了,但是本节点的子节点仍需要进行更新。
- 4.线段树的存储结构:线段树是用数组来模拟树形结构,对于每一个节点R,左子节点为 2R (一般写作R<<1), 右子节点为 2R+1(一般写作R<<1|1)
然后以1为根节点,所以,整体的统计信息是存在节点1中的,节点0通常不做使用。
牢记左子树和右子树与根节点的索引关系 [0,1,2,3,4,5,6,7,8,9],这里构建一个[1,5]的线段树,可以看下左子树和右子树的索引关系
2.实现
线段树的实现有两种方式:递归和非递归,递归从上到下,根节点到叶子节点;非递归从下到上,从叶子节点到根节点
递归实现
自上而下递归构建线段树
#define n 10007
int sum[n<<2], add[n<<2]; // add为懒惰标记
int a[n], num;
void pushUp(int rt){sum[rt]=sum[rt<<1]+sum[rt<<1|1];}
//自上而下构建线段树
void build(int l, int r, int rt){
if(l==r){
sum[rt] = a[l];
return;
}
int m = (l+r)>>1;
build(l, m, rt<<1);
build(m+1, r, rt<<1|1);
pushUp(rt);
}
更新点位
// 更新点位 a[L] += c
void update(int L, int c, int l, int r, int rt){
if(l==r){//叶子节点
sum[rt] += c;
return;
}
int m = (r+l)>>1;
if(L<=m) update(L, c, l, m, rt<<1);
else update(L, c, m+1, r, rt<<1|1);
pushUp(rt);
}
下推标记,递归线段树是自上而下,该函数是计算add数组存储的子节点变动值,也就是将标记值下推
// ln rn分别为左端点和右端点
void pushDown(int rt, int ln, int rn){
if(add[rt]){
//下推标记
add[rt<<1] += add[rt];
add[rt<<1|1] += add[rt];
//修改子节点的值
sum[rt<<1] += add[rt]*ln;
sum[rt<<1|1] += add[rt]*rn;
add[rt] = 0; // 清除当前标记
}
}
区间修改 a[L,R] += c
void update(int L, int R, int c, int l, int r, int rt){
if(L<=l && r<=R){ //区间位于[L,R]内
sum[rt] += c*(r-l+1);
add[rt] += c;//add标记,表示本区间的sum正确,子区间的sum需要更新
return;
}
int m = (l+r)>>2;
pushDown(rt, m-l+1, r-m);//下推之前的标记,更新值
if(L<=m) update(L, R, c, l, m, rt<<1);
if(R>m) update(L, R, c, m+1, r, rt<<1|1);
pushUp(rt);
}
区间查询
int query(int L, int R, int l, int r, int rt){
if(l>=L && r<=R) return sum[rt];
int m = (l+r)>>1;
pushDown(rt, m-l+1, r-m);
int res=0;
if(R>m) res += query(L, R, m+1, r, rt<<1|1);
if(L<=m) res += query(L, R, l, m, rt<<1);
return res;
}
非递归实现
非递归实现这里采用一种简洁的方式,假设原数组长度为n,则定义一个长度为2n的数组存储线段树
非递归线段树构建、点位修改和区间查询都比较简洁,所以能非递归尽量采用非递归
- 这里我采用的非递归构建方式,左节点索引始终为偶数,右节点索引始终为奇数
// n为原数组的长度
void build() {
for(int i=0;i<n;i++){
for(int i=0;i<n;i++){
sum[i+n] = a[i];
}
for(int i=n-1;i>0;i--){
sum[i] = sum[i<<1]+sum[i<<1|1];
add[i]=0;
}
}
}
//点修改 a[L]+=c
void update(int L, int c){
for(int i=L+n;i>0;i>>=1){
sum[i] += c;
}
}
//点修改下的区间查询
int query(int L, int R){
int res=0;
for(int l=L+n, r=R+n;l^r^1;l>>=1, r>>=1){
if(l&1) res += sum[l++];
if(~r&1) res += sum[r--];
}
return res;
}
非递归的区间修改和区间查询编写是较复杂,原因在于非递归是自下而上,很难在一个区间传递变更值,这里借助add标记来完成
//区间修改
void update(int L, int R, int c){
int l=n+L, r=n+R, Ln=0, Rn=0, x=1;
for(;l^r^1;l>>=1, r>>=1, x<<=1){
sum[l] += c*Ln; // 加上舍弃的节点的值
sum[r] += c*Rn;
if(l&1) { // 左端点是右子节点时,舍弃该节点
add[l]+=c; // 标记该节点,该节点子节点需要加c
sum[l++]+=c*x; // 更新该节点的值
Ln+=x;
}
if(~r&1){
add[r]+=c;
sum[r--]+=c*x;
Rn+=x;
}
}
for(;l;l>>=1,r>>=1){
sum[l]+=c*Ln;
sum[r]+=c*Rn;
}
}
// 查询思路与区间更新一致类似,但不需要更新sum值
int query(int L, int R){
int l=n+L,r=n+R,Ln=0,Rn=0,x=1;
int res=0;
for(;l^r^1;l>>=1,r>>=1,x<<=1){
if(add[l]) res+=add[l]*Ln;
if(add[r]) res+=add[r]*Rn;
if(l&1) {
res+=sum[l++];
Ln+=x;
}
if(~r&1) {
res+=sum[r--];
Rn+=x;
}
}
for(;l;l>>=1,r>>=1){
res+=add[l]*Ln;
res+=add[r]*Rn;
}
return res;
}
以上代码根据借鉴的思路写的,还未测试,有问题欢迎指正。