zkw线段树
一、初步认识
zkw线段树是普通线段树的更新,具有普通线段树的绝大多数性质。
主要区分点就是:
- 普通线段树多用递归实现,zkw线段树主要用循环实现
- 拿求区间最小值来说,普通线段树采用一个结点表示一段区间的最小值,而zkw不是的,详细的可以看下面的对比图。
下面给出普通线段树和zkw线段树图形的表示对比。Array[]表示建树时使用的数组。
zkw线段树有以下几个优点
- 非递归
- 效率高,整体(建树+更新+查询)效率高
- 代码短,背板子也是轻松许多
但很显然,代码短了,理解难度就会上去。zkw线段树也不例外,同树状数组有着相同的本质,也是使用了二进制的一些思想。
二、二进制复习
由于该方法中涉及到很多二进制,在这先给大家复习一下二进制运算的知识点。
-
与运算:&
两者都为1则为1,否则为0 1&1 = 1,1&0 = 0, 0&1 = 0,0 & 0 = 0
-
或运算:|
两者都为0为0,否则为1 1 | 1 = 1, 1 | 0= 1, 0 | 1 = 1, 0 | 0 = 0
-
非运算:~
1取0 0 取1 ~1 = 0 ~ 0 = 1 ~(1001) = 0110
-
异或运算:^
两者相等为0,不等为1 1^1 = 0 1^0= 1 0^1 = 1 0^0 = 0
-
位移符号:<<、>>
<<左移若干位位,0补 >>右移若干位 101<<1 = 1010 101>>1 = 10
在我们下面的代码中可能会出现以下几种情况(仅解释其在完全二叉树中的含义):
s<<1 //s×2
s>>1 //s÷2
s|1、s^1 //s的右兄弟结点(若s为左儿子结点坐标)坐标
s<<1|1、s<<1^1 //s的右儿子结点坐标
s&1^1 //判断s是否为左孩子结点
t&1 //判断t是否为右孩子节点
三、代码初识
和普通线段树一样,主要有几个函数
- 建树
- 查询
- 更新
以求区间最小值为目的建树,mn[]数组表示树,有效下标从1开始:
建树:
inline void build() {
//m记录的是zkw线段树存放array[]的那一行的第一个结点在mn[]中的下标
//之所以通过2的累乘可以实现,是完全二叉树的性质决定的。
for(m=1;m<=n;m<<=1);
for(int i=m+1;i<=m+n;++i)
cin >> mn[i];
for(int i=m-1;i;--i){
mn[i]=min(mn[i<<1],mn[i<<1|1]);
//这里采用了差分思想,有助于后续结点更新
mn[i<<1]-=mn[i],mn[i<<1|1]-=mn[i];
}
}
所谓差分思想就是,结点存储的为与父结点的差。
采用差分思想之后,得到的图如下:
更新区间(在s-t区间范围内,给每个结点的值增加v):
inline void update_part(int s,int t,int v){
int A=0; //A是作中间变量的
/* for的第一步是将闭区间[2,5]->(3,6);
* 好处就是在后面进行讨论的时候,只需要考虑s的右兄弟,t的左兄弟,没有的话,就不讨论
* 为了避免重复,我们只需要将for循环的终止条件设置成s、t是兄弟结点
*/
for(s+=m-1,t+=m+1;s^t^1;s>>=1,t>>=1) {
if(s&1^1) mn[s^1]+=v; //对于s,只考虑其右兄弟
if(t&1) mn[t^1]+=v; //对于t,只考虑其左兄弟
//下面两行代码主要是为了差分
A=min(mn[s],mn[s^1]),mn[s]-=A,mn[s^1]-=A,mn[s>>1]+=A,
A=min(mn[t],mn[t^1]),mn[t]-=A,mn[t^1]-=A,mn[t>>1]+=A;
}
//当s、t成为兄弟结点之后,继续往上回溯,知道根结点,将所有的结点都通过差分进行更新
for(;s>1;s>>=1) {
A=min(mn[s],mn[s^1]),mn[s]-=A,mn[s^1]-=A,mn[s>>1]+=A;
}
}
单点值查询:
inline int query_node(int x,int ans=0){
//建树之初,为了方便后续更新结点,采用的是差分的方法对结点进行赋值
//所以我们这单点查询就是从结点开始不断回溯到根结点,将途径的值加起来就是该点的值了
for(x+=m;x;x>>=1) ans+=mn[s];
return ans;
}
区间最小值查询:
inline int query_min(int s,int t,int L=0,int R=0,int ans=0) {
if(s==t) return query_node(s); //单点要特判, 防止s、t不能成为兄弟结点,死循环
//这里的区间不需要变为闭区间
for(s+=m,t+=m;s^t^1;s>>=1,t>>=1) {
//L和R左右开工,分别代表s这一边和t这一边的最小值
L+=mn[s],R+=mn[t];
if(s&1^1) L=min(L,mn[s^1]);
if(t&1) R=min(R,mn[t^1]);
}
/* for循环退出后,此时的s和t为兄弟结点,
* L和R分别记录了s侧和t侧的最小值,
* 还是由于使用了差分的思想,我们选择其中最小的一个值,继续往上回溯到根结点,并进行累加
*/
for(ans=min(L,R),s>>=1;s;s>>=1) ans+=mn[s];
return ans;
}
洛谷线段树入门题:https://www.luogu.com.cn/problem/P3372
可以尝试一下用zkw线段树来写,这里我就不展示了