我尽力让它是个学习笔记:ZKW线段树

前言

我研究了一晚上的ZKW线段树,总要有点收获吧,打算就现在这里啦。

先说一下我为什么要学这个ZKW线段树,因为好写,对于我来说,就是为了好写。不过后来发现它还有更多的好处,比如非递归啊,常数小啊,是不是还可以可持久化!?当然我觉得这不是很科学,因为堆储存父亲是确定的, 可能只是运用到了一些技巧罢。

但是它有什么局限性呢,其实应该是有的,ZKW在他的PPT《统计的力量》里面曾经自称他写的这个是树状数组,是有道理的,这是一个加强版的树状数组,可以实现一部分的线段树功能。我再研究研究吧。。。。

之后将会以RMQ为主,因为。。。区间求和用树状数组去啊!

非差分版本(不支持区间修改)

特点

利用了堆式储存,利用了二叉堆中结点本身的数值关系。

开的点数一定要是2的倍数

原理

我不想写原理了,网上到处都是,原版的可以参见ZKW的PPT《统计的力量》,能看到我这篇博文的应该都是自学ZKW线段树学得实在没有办法开始乱找资料的那种,原理大概是了解了吧,我就写个版子,然后把我学习过程中的疑问提一下。

我先来解释一下不容易理解的地方,可以先跳过,看不懂代码的地方再回来看这里。

代码基本上和网上保持一致。


问:为什么要开区间维护和查询/为什么从M+1的结点开始编号?(M是叶子结点的个数也是最后一排第一个元素的下标)

答:我们把[l,r]变成(l-1,r+1),在我们的代码中初始结点并不会被统计到,所以实际上统计的是[l,r],由于这个性质,我们从M+1开始编号并且把叶子结点最后一个位置空出来(因为我们无法统计这两个位置的元素)。也说明我们最终只能维护M-2个结点(n是叶子结点的个数)。还有一个好处就是第i个元素的下标是M+i,多方便啊。


关于查询操作是基于这么一个思想:如果一个左边结点是父亲结点的左子树,那么他的兄弟结点就一定需要被考虑。

在具体操作中为了防止重复统计,当左右两个结点是兄弟结点的时候,就停止了。

可以结合网上其他人的图理解哦。


还有一点代码技巧,不要问为什么,可以仔细研究一下满二叉树的编号特征和位运算符

简单的结点关系:

‘x>>1’:x/2

‘x<<1’:x*2

‘x|1’:x是奇数不变,偶数则+1

一些判断子树的技巧:

‘x&1’:x%2==1

‘~x&1’:x%2==0

i和j是兄弟结点:

i^j=1

i^1=j

j^1=i

i^j^1=1

左子树:now<<1

右子树: now<<1|1

父亲结点:now>>1

i结点对应叶子结点编号:M+i

版子

#define lc now<<1
#define rc now<<1|1
const int maxn=1e5+100,inf=1e9;
int a[maxn];
struct ZKW_SegmentTree
{
    static const int maxn=1<<(17+1);
    int M,mi[maxn];
    void pushup(int now)
    {
        mi[now]=min(mi[lc],mi[rc]);
    }
    void Build(int n)//直接和Initial合并;这里是唯一需要用到n的地方,后面都不用了,当然可以不加这个参数
    {
        for(M=1;M-2<n;M<<=1);//只能统计M-2个元素 
        memset(mi,0x3f,sizeof(mi)) ;
        for(int i=1;i<=n;i++)mi[i+M]=a[i];//填满儿子结点 
        for(int i=M-1;i;i--)pushup(i);//填满父亲结点 
    }
    void modify(int i,int v)//把i的值增加v 
    {
        mi[M+i]+=v;
        for(int now=(M+i)>>1;now;now>>=1)pushup(now);
    }
    int query(int i,int j)
    {
        int ret=inf;
        for(i=i+M-1,j=j+M+1;i^j^1;i>>=1,j>>=1)
        {
            if(~i&1)ret=min(ret,mi[i^1]);
            if(j&1)ret=min(ret,mi[j^1]);
        }
        return ret;
    }
}sgt;

代码解释:

看注释

差分版本(支持区间修改)

先说说

写给那些看了原理理解不了开始乱翻博客的人看的。
至于原理就是差分嘛,代码可以看我的。
原理就懒得详细写了,但是我会写一下很细节理解,都是我学习的时候遇到的问题。

操作原理与细节理解

我来理解一下这个ZKW线段树的区间修改,以最小值为例子

定义原始值是每个结点代表区间的最小值,差分值是当前结点原始值减去父亲结点原始值,令根结点的差分值=原始值

我们可以通过计算该结点和其所有祖先的差分值,得到一个元素的原始值
这也就可以省去原始值数组,也就是ZKW说的:永久化标记就是值

通过差分的思想就很容易修改了:
对于一个区间:
如果所有的元素都被修改,那么差分之后该结点的所有子结点的差分值都不变,只需要将该结点的差分值增加v
这是基于这样的性质,我们才有可能在log级别的时间复杂度完成修改

关于修改过程的理解:
在修改的一个过程中,我们是逐步向上的,也就是说我们的当前线段树的值
原始值只有当前结点和子结点的原始值是对的,而祖先结点我们还没有修改。
我们通过上传操作来完成对父亲结点原始值的修改。

关于上传操作的理解(再次强调是对于min操作,其它的略有不同,超易推):
我们来看一看一颗完全正确的树长成什么样:
少主经过缜密的研究发现有很多结点的差分值是0!这说明有很多子结点和父亲结点的值一样!而且它的兄弟结点的值是正数(max就是负数了哈)!(惊恐.jpg)
咳咳。。。废话!父亲结点的原始值肯定来自的原始值较小的那个儿子啊。

重点来了!!!
所以修改的时候我们把较小的那个儿子的差分值弄成0,假设减小了A,然后由于原始值不变,我们把父亲结点的差分值增加A,又由于原始值不变,我们把该结点的兄弟结点(父亲结点的另一个儿子)的差分值减小A。
其中由于两个兄弟结点的祖先是一样的,我们可以直接通过差分值判断大小。

关于上传操作的对象:
这里我们上传操作是修改父亲结点的值,而不是递归线段树中修改当前结点的值
并且想一下你就会发现,其实也是可以放在父亲结点修改的。
那么为什么ZKW要写成对父亲结点的操作呢?
其实这个和他要用开区间查询的道理是一样的,为了不在最后一排讨论。

如果在子结点修改父亲,只会在根结点顺便把0修改了。
而如果在父亲结点取子结点,一旦不在叶子结点特判的话,就会访问违规下标。

不过我这种懒人并不打算这么做
因为pushup操作实在是太好写了,叶子结点也很好判断,况且我们可以在建树操作用同样的操作完成。

关于查询:
由于是差分值,因此不要忘了即使找到了最优值,还要加上祖先的值才得到原始值哦

关于单点查询:
单点查询可以用区间查询,这应该也是个用开区间的原因,感觉很方便
非要单独查就把它和祖先的差分值加起来即可。

代码

关于代码

因为支持区间修改维护的是差分值,建图修改查询和非差分版全部都不一样

还有就是少主我的代码是最大值哈,那个。。因为少主家题库是最大值嘛。。

typedef long long LL;
#define lc now<<1
#define rc now<<1|1
const LL inf=1e16;
const int maxn=1e6+10;
int a[maxn];
struct ZKW_SegmentTree
{
    static const int maxn=(1<<20)+10;
    int M;
    LL mx[maxn<<1];
    void pushup(int now)
    {
        if(now>M)return;
        LL A=max(mx[lc],mx[rc]);
        mx[lc]-=A,mx[rc]-=A,mx[now]+=A;
    }
    void Build(int n)
    {
        for(M=1;M-2<n;M<<=1);
        for(int i=0;i<M;i++)mx[i+M]=-inf;
        for(int i=1;i<=n;i++)mx[i+M]=a[i];
        for(int i=M-1;i;i--)pushup(i);
    }
    void modify(int s,int t,int v)
    {
        for(s=s+M-1,t=t+M+1;s^t^1;s>>=1,t>>=1)
        {
            pushup(s);pushup(t);
            if(~s&1)mx[s^1]+=v;
            if(t&1)mx[t^1]+=v;
        }
        pushup(s);pushup(t);
        for(s>>=1;s;s>>=1)pushup(s);
    }
    LL query(int s)//单点查询 
    {
        LL ret=0;
        for(int i=s+M;i;i>>=1)ret+=mx[i];
        return ret;
    }
    LL query(int s,int t)//区间查询 
    {
        if(s==t)return query(s);
        LL L=0,R=0;
        for(s=s+M,t=t+M;s^t^1;s>>=1,t>>=1)
        {
            L+=mx[s],R+=mx[t];
            if(~s&1)L=max(L,mx[s^1]);
            if(t&1)R=max(R,mx[t^1]);
        }
        L+=mx[s],R+=mx[t]; 
        LL ret=max(L,R);
        for(s>>=1;s;s>>=1)ret+=mx[s];
        return ret;
    }
}sgt;

依旧算是短小精干,但是总感觉考虑了很多特殊情况,出现了诸多问题,而且区间查询的时候不能用开区间,代码肯定是没有问题的,但是问题在于有没有更加简单并且一般的方法,因为这样子感觉可能会比较容易写错。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值