zkw 线段树

zkw 线段树

参考博文

俗称 重口味 ,与 KMP 类似,咳咳...

一、ZKW线段树简介

ZKW线段树是由清华大学姚班大佬 张昆玮 所创立的一种线段树储存结构,由于其基于非递归的实现方式以及精简的代码和较高的效率而闻名。甚至,ZKW线段树能够可持久化。

我们从算法的角度对基础线段树进行分析:其实线段树算法本身的本质仍是统计。因此我们可以从统计的角度入手对线段树进行分析:线段树是将一个个数轴划分为区间进行处理的,因此我们面对的往往是一系列的离散量,这导致了我们在使用时的线段树单纯的退化为一棵"点树"(即最底层的线段树只包含一个点)。基于这一点可以入手对线段树进行优化。

二、ZKW线段树的构造原理

首先,我们忽略线段树中的数据,从线段树的框架结构入手进行分析:如图所示是一颗采用堆式储存的基本线段树:

我们将节点编号转换为二进制:

观察转为二进制后的结点规律:在基础线段树的学习中,我们知道对于任意结点x,其左子节点为x<<1,右子节点为x<<11。这个规律是我们从根结点出发向叶节点寻找的规律。那么现在我们换个思路:从叶结点出发向根结点寻找规律:

  • 当前结点的父节点一定是当前的结点右移一位(舍弃低位)得到的
  • 当前结点的左子节点为x<<1,右子节点为x<<11
  • 每一层结点按照顺序排列,第n层有2n1个节点
  • 最后一层的结点个数 = 值域

因为最后一层的结点个数=值域,假设给定数组a[n],含有元素a[1]a[n]

我们约定,无论元素的个数是否达到2n,最后一层的空间都开到2n,无数据的叶节点空置即可。

三、ZKW线段树基本操作

1.建树操作

void build(int n){
    for(m = 1; m <= n;) m <<= 1;
    for (int i = m + 1; i <= m + n; ++i) op_array[i] = read();
    for (int i = m - 1; i; --i) operation(),
}
  • 如果维护区间和,那么op_array[]sum[]operation
sum[i] = sum[i << 1] + sum[i << 1 | 1];
  • 如果维护区间最小值,那么op_array[]minn[]operation
minn[i] = min(minn[i << 1], minn[i << 1 | 1]);	//不支持修改操作
minn[i] = min(minn[i << 1], minn[i << 1 | 1]),
minn[i << 1] -= minn[i], minn[i << 1 | 1] -= minn[i];
  • 如果维护区间最大值,那么op_array[]maxx[]operation
maxx[i] = max(maxx[i << 1], maxx[i << 1 | 1]);	//不支持修改操作
maxx[i] = max(maxx[i << 1], maxx[i << 1 | 1]),
maxx[i << 1] -= maxx[i], maxx[i << 1 | 1] -= maxx[i];

2.单点查询

这个操作是相对容易理解的,就是一个从叶子结点开始,不断向父节点走,同时累加沿路的权值的过程。

int query(int x){
    int ans = 0;
    for (x += m; x; x >>= 1) ans += minn[s];
    return ans;
}

3.单点修改

单点修改的思路非常简单,只需要修改当前结点并更新父节点即可。

void update(int x,int v){
    op_array[x = m + x] += v;
    while(x) operation();
}
  • 如果维护区间和,那么op_array[]sum[]operation:
sum[i] = a[i << 1] + a[i << 1 | 1];
//如果单纯维护区间和,那么可以压行:
void update(int p, int k){ for (p += m; p; p >>= 1) sum[p] += k; }
  • 如果维护区间最小值,那么op_array[]minn[]operation
minn[i] = min(minn[i << 1], minn[i << 1 | 1]),
minn[i << 1] -= minn[i], minn[i << 1 | 1] -= minn[i];
  • 如果维护区间最大值,那么op_array[]maxx[]operation
maxx[i] = max(maxx[i << 1], maxx[i << 1 | 1]),
maxx[i << 1] -= maxx[i], maxx[i << 1 | 1] -= maxx[i];

4.区间查询

如何进行区间查询?我们继续二进制表示入手,寻找查询的规律。

在实际的查询中,我们采取扩增左右区间端点的方式进行查询,即:将闭区间转换为开区间查询。

我们以下图为例:假设要查询的区间为[1,2],那么首先转换为开区间(0,3),我们可以发现变为开区间之后,0的兄弟结点必在区间之内,3的兄弟结点​必在区间内;根据这个规律我们可以总结:

对于待查区间[l,r]

  • 如果l是左儿子,则其兄弟结点必位于区间之内;
  • 如果r是右儿子,则其兄弟结点必位于区间之内;
  • 查询的终止条件:两个结点同为兄弟;
  • 以上结论,对于任意层的结点均成立。

我们通过例子来模拟这个过程:

在如图所示的ZKW线段树中,假设我们要查询区间[1,4],那么步骤如下:

  • 闭区间改开区间,[1,4]改为查询(0,5),扩增至(M+0,M+5)=(8,13)

  • 判断:左端点D[1000B]是左儿子,那么其兄弟D[1001B]必位于区间内,累加ans+=D[1001B]
    判断:右端点D[1101B]是右儿子,那么其兄弟D[1100B]必位于区间内,累加ans+=D[1100B]

  • 缩小区间(向根结点缩):l>>=11000>>1=0100r>>=11101>>1=0110

  • 判断:左端点D[0100B]是左儿子,那么其兄弟D[0101B]必位于区间内,累加ans+=D[0101B]
    判断:右端点D[0110B]是左儿子,不做操作;

  • 缩小区间(向根结点缩):l>>=10100>>1=0010r>>=10110>>1=0011

  • 此时lr同为兄弟,因此终止查询。

我们可以总结出区间查询的步骤:

  • 闭区间改开区间[l,r](l+M1,r+M+1)
  • 判断当前区间左端点是否是左儿子,如果是,则向累加器中累加兄弟结点;
    判断当前区间右端点是否为右儿子,如果是,则向累加器中累加兄弟结点;
  • 端点变量处理操作:l>>=1,r>>=1
  • 循环执行23的步骤,直到lr同为兄弟结点(此时不终止会导致重复计算)

如何判断是否为左子节点?我们很容易观察到左右子节点共同的特征:左子节点最低位为0 ,右子节点最低位为1,那么我们可以通过以下操作的真值判断左右子节点

  • 判断左子节点:∼l & 1
    l % 2 == 0的意思
  • 判断右子节点:r & 1
    r % 2 == 1的意思

对于取兄弟结点的值则可以通过与1异或求得:

  • 左子节点求兄弟结点:l xor 1
  • 右子节点求兄弟结点:r xor 1

TODO 文档先写到这里,先去看一代码,理解清楚后再继续研究
2023.01.05

https://blog.csdn.net/qcwlmqy/article/details/95938283

https://blog.csdn.net/weixin_43823476/article/details/92833064

https://www.acwing.com/blog/content/25173/

https://blog.csdn.net/yanweiqi1754989931/article/details/117575178

https://zhuanlan.zhihu.com/p/29876526
https://zhuanlan.zhihu.com/p/29937723

HDU 1166 敌兵布阵 单点更新,区间查询

posted @   糖豆爸爸  阅读(142)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
历史上的今天:
2022-01-05 AcWing 320 能量项链
2016-01-05 试题识别与生成
Live2D
点击右上角即可分享
微信分享提示