zkw线段树学习前提须知
该线段树几乎可以处理线段树的所有问题,比线段树的速度快很多,但比树状数组的速度慢,而且,代码超短!
zkw线段树不能处理 有运算优先级的问题 ,可以说吊打线段树
算法内容
zkw线段树略讲
详细可以参考 洛谷日报 讲的很棒,但是图中没有二进制图,要二进制图可以参考 有二进制图的zkw线段树
zkw线段树相对于原本的线段树不同的是,它是一棵完全二叉树,是真正的要开满四倍空间的线段树,该线段树是将所有节点转化为了二进制的形式,然后对于每个叶子节点,我们把值存进去,听起来感觉和线段树差不多,那为什么快呢?原理你可以看上面两个博客,我这里对代码进行分析
【单点修改 + 区间查询】
首先是 建树
//#define fre yes
#include <cstdio>
int N = 1;
inline void build(int n) {
for (; N <= n + 1; N <<= 1);
for (int i = N + 1; i <= N + n; i++) read(tree[i]);
for (int i = N - 1; i >= 1; i--) tree[i] = tree[i << 1] + tree[i << 1 | 1];
}
先来解决几个疑问,N是个什么东西?我们的zkw线段树是一个完全二叉树,而我们也是对一个完全二叉树的叶子节点填数的,那我们填数一定是在叶子节点上进行填数,我们都知道对应完全二叉树上的叶子节点的编号,是原数组编号加个固定的常数(可以自己试试?) 那这个常数是怎么计算呢?通过下面的式子
那这样我们就能得到这个N,于是上面的式子是怎么出来的就很显然了。
接下来说怎么搞这个单点修改
inline void modify(int x, int k) {
for(x += N; x; x >= 1) tree[x] += k;
}
好了,我觉得也不用解释了,很简单..
接下来是区间查询
单点修改的区间查询相对于简单很多
int query(int s, int t) {
int ans = 0;
for (s = N + s - 1, t = N + t - 1; s ^ t ^ 1; s >>= 1, t >>= 1) {
if(~s & 1) ans += tree[s ^ 1];
if(t & 1) ans += tree[t ^ 1];
} return ans;
}
我们的区间查询遵守一个规定就是
1、s指向的节点是左儿子,那么ans += 右儿子的值
2、t指向的节点是右儿子,那么ans += 左儿子的值
(看图我们知道 左儿子最后一位为0,右儿子最后一位为1)
这里右儿子,左儿子都是针对同一深度的两个儿子而言的(这里是关键) 不然不好理解上面的代码
这里用别人博客的图片对着看一下吧,文字和其他的都不用管 就关注一下每个节点的二进制
~s是指取反 10011 -> 01100
然而我们发现我们只能查询[1, n-1]
如果想查询[0, m] 我们就将数组整体平移
如果想要查询[m, n] 直接将N扩大两倍
【区间修改,区间查询】
首先是建树(和上面一样 这里略)
区间修改
这里需要用到 标记永久化 的思想,就是不下推Lazy标记,让其一直存在
含义和区间修改的线段树差不多,这里就不举例子了,直接看代码吧
inline void update(int s, int t, int k) {
int lNum = 0, rNum = 0, nNum = 1;
for (s = N + s - 1, t = N + t + 1; s ^ t ^ 1; s >>= 1, t >>= 1, nNum <<= 1) {
tree[s] += k * lNum;
tree[t] += k * rNum;
if(~s & 1) {
add[s ^ 1] += k;
tree[s ^ 1] += k * nNum;
lNum += nNum;
}
if(t & 1) {
add[t ^ 1] += k;
tree[t ^ 1] += k * nNum;
rNum += nNum;
}
}
for (; s; s >>= 1, t >>= 1) {
tree[s] += k * lNum;
tree[t] += k * rNum;
}
}
哎,这个区间修改我知道大家都有很问题,我们一个一个来解决
首先是这个 lNum, rNum, nNum 分别是什么意思
lNum表示的意思是,从s一路走来已经包含了几个数
rNum表示的意思是,从t一路走来已经包含了几个数
nNum表示的意思是,本层中每个节点包含几个数(就是一个深度的所有父亲节点所包含的儿子数目)
第一个转移 tree[s] += k ( imes) lNum 和 tree[t] += k ( imes) rNum 是什么意思呢?
很明显,每次我们都是倒着处理的,那么这个转移就是很明显了,可以类比线段树
第二个转移 两个if里面的式子变了,我们来一个一个分析
首先是add,这很显然,我们每次要把之前的指针往上传,但是原本的不变
然后是tree,这也很显然,和第一个转移一样
然后是rNum 和 lNum,这个转移又是什么意思呢,我们知道我们只会遍历到一个路径上,那么其中肯定有一个父亲节点的另外一棵子树不会被遍历到,但是实际上这些点也是在我们的修改范围内的,所以我们这里要这么统计,方便我们对rNum, lNum进行操作
最后我们再将两个点走到根节点就好了
最后是区间查询
和上面差不多,这里就放代码辣
int query(int s, int t) {
int lNum = 0, rNum = 0, nNum = 1;
int ans = 0;
for (s = N + s - 1, t = N + t + 1; s ^ t ^ 1; s >>= 1, t >>= 1, nNum <<= 1) {
if(add[s]) ans += add[s] * lNum;
if(add[t]) ans += add[t] * rNum;
if(~s & 1) {
ans += tree[s ^ 1];
lNum += nNum;
}
if(t & 1) {
ans += tree[t ^ 1];
rNum += nNum;
}
}
for (; s;s >>= 1, t >>= 1) {
ans += add[s] * lNum;
ans += add[t] * rNum;
} return ans;
}
是不是和上面很像,原理都是一样的 可以类比线段树区间查询
上面仅仅是举了一个区间求值的例子,当然也可以引申到求最大最小值上