『zkw线段树』zkw线段树略讲

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 = 2^\left \lceil log_2^{n + 1} \right \rceil \]

那这样我们就能得到这个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 \(\times\) lNum 和 tree[t] += k \(\times\) 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;
}

是不是和上面很像,原理都是一样的 可以类比线段树区间查询

上面仅仅是举了一个区间求值的例子,当然也可以引申到求最大最小值上

posted @ 2019-10-10 19:15  Nicoppa  阅读(357)  评论(0编辑  收藏  举报