zkw线段树

zkw线段树

一、初步认识

zkw线段树普通线段树的更新,具有普通线段树的绝大多数性质。

主要区分点就是:

  1. 普通线段树多用递归实现,zkw线段树主要用循环实现
  2. 求区间最小值来说,普通线段树采用一个结点表示一段区间的最小值,而zkw不是的,详细的可以看下面的对比图。

下面给出普通线段树和zkw线段树图形的表示对比。Array[]表示建树时使用的数组。

在这里插入图片描述

zkw线段树有以下几个优点

  1. 非递归
  2. 效率高,整体(建树+更新+查询)效率高
  3. 代码短,背板子也是轻松许多

但很显然,代码短了,理解难度就会上去。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线段树来写,这里我就不展示了

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值