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 !

尾声

如果你发现了问题,你可以直接回复这篇文章!

posted @ 2022-04-07 17:38  zsc985246  阅读(593)  评论(0编辑  收藏  举报