zkw线段树
本文网址:https://www.cnblogs.com/zsc985246/p/16112689.html ,转载请注明出处。
zkw线段树就是非递归的线段树。
我们先来看看普通的线段树:
换成二进制看看?
这种存储方式及这三条规律便是zkw线段树的核心原理。
建树
我们通过上图可以发现,我们令 \(m=⌈1<<log_2(n)⌉\),只需要从 \(1+m\) 开始遍历,到 \(n+m\) 结束,就可以成功的建树了。
接下来考虑维护其它节点的信息。可以直接从上图看出, \(m\) 其实是第一个叶子节点,而所有的非叶子节点下标都小于 \(m\) 。通过这一点,我们可以直接从 \(m-1\) 遍历到 \(1\) 访问其它节点并维护信息。
void build(){//建树
while(m<=n)m<<=1;//1+1<<ceil(log(n))
for(int i=m+1;i<=m+n;i++){
scanf("%d",sum[i]);//这里维护区间和
}
for(int i=m-1;i;i--){//遍历非叶子节点
sum[i]=sum[i<<1]+sum[i<<1|1];//维护
}
}
单点修改
第三条规律:修改第 \(x\) 个元素对应到线段树上就是修改下标为 \(x+m\) 的数。
第二条规律:我们可以用 \(x>>=1\) 找到 \(x\) 的父亲,然后维护信息。
void change_point(int x,int pos){//a[x]=pos
x+=m;//找对应叶子节点
sum[x]=pos;//更新
for(x>>=1;x;x>>=1){//向上更新
sum[x]=sum[x<<1]+sum[x<<1|1];//维护
}
}
区间修改(区间加法)
跟普通的线段树差别不大,也是开一个 \(add\) 数组,记录这颗子树的加法操作。
但是,我们这是非递归,所以我们需要换种方式。
首先我们先把这个闭区间 \([l,r]\) 转换成开区间 \((l-1,r+1)\)。
转换成开区间 \((l-1,r+1)\) 后,令 \(s=l-1,t=r+1\)。
如果 \(s\) 是它父亲的左儿子,就更新它的兄弟节点 \(s^1\),然后 \(s\) 再跳到父亲节点,重复执行此操作。
\(t\) 同理。
注意同时进行 \(s\) 和 \(t\) 的操作。
借助图理解一下(红色代表 \(add\) 需要更改):
然后对红色部分的 \(add\) 更改即可。
等等,好像没有下传标记——
实际上,我们不需要下传标记。我们可以让标记永久化。在询问时,如果此节点的 \(add\) 有值,答案加上 区间长度\(\times add\) 的值 即可。
注意,当 \(s\) 和 \(t\) 的父亲相同时,虽然不继续向上跳了,但还需要继续维护 \(sum\) 的值。
void change_part(int l,int r,int pos){//a[s~t]+=pos
int s=l+m-1,t=r+m+1;//s和t
int len=1;//当前层区间包含数的个数
int lcnt=0;//当前s的子树中更改的数的个数
int rcnt=0;//当前t的子树中更改的数的个数
while(s^t^1){//t^1是t的兄弟节点,因为a^a=0,所以s^t^1=0代表s和t父亲相同
if(s&1^1){//s&1取末位(0为左儿子,1为右儿子),再^1表示兄弟节点
add[s^1]+=pos;//打标记
lcnt+=len;//更改了len个数
}
if(t&1){//原理与上相同
add[t^1]+=pos;//打标记
rcnt+=len;//更改了len个数
}
//维护
sum[s>>1]+=pos*lc;
sum[t>>1]+=pos*rc;
s>>=1,t>>=1;//跳到父亲节点
len<<=1;//包含的节点个数*2
}
int cnt=r-l+1;//总共更改的数的个数
s>>=1;//父节点
while(s){
sum[s]+=cnt*pos;//维护
s>>=1;//父节点
}
}
单点查询
这不用多说,直接返回即可。
int query_point(int x){
x+=m;
return sum[x];
}
区间查询
与区间修改思路相同。
主要处理在区间修改时提到的标记永久化。
由于标记是打在一颗子树上,所以整颗子树总共加了 区间长度\(\times add\) 的值 。
ll query(int l,int r){//区间求和
ll tmp=0;//临时答案
int s=l+m-1,t=r+m+1;//s和t
int len=1;//当前层区间包含数的个数
int lcnt=0;//当前s的子树中更改的数的个数
int rcnt=0;//当前t的子树中更改的数的个数
while(s^t^1){
if(s&1^1){
tmp+=sum[s^1]+len*add[s^1];//统计
lcnt+=len;//更改了len个数
}
if(t&1){
tmp+=sum[t^1]+len*add[t^1];//统计
rcnt+=len;//更改了len个数
}
s>>=1,t>>=1;
len<<=1;
}
int cnt=lcnt+rcnt;//总共更改的数的个数
s>>=1;//父节点
while(s){
tmp+=add[s]*cnt;//统计
s>>=1;//父节点
}
return tmp;
}
zkw线段树与线段树的对比
代码长度 | 理解难度 | 时间 | 空间 | |
---|---|---|---|---|
zkw线段树 | 短 | 稍高 | 快 | 小 |
线段树 | 长 | 低 | 稍慢 | 稍大 |
%%%zkw !
尾声
如果你发现了问题,你可以直接回复这篇文章!