0
点赞
收藏
分享

微信扫一扫

zkw 线段树

\(zkw\)

​​参考博文​​

俗称 重口味 ,与 \(KMP\)

一、\(ZKW\)线段树简介

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

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

二、\(ZKW\)线段树的构造原理

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

zkw 线段树_线段树

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

zkw 线段树_结点_02

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

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

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

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

三、\(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[] \rightarrow sum[],operation\)

sum[i] = sum[i << 1] + sum[i << 1 | 1];

  • 如果维护区间最小值,那么\(op\_array[] \rightarrow 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[] \rightarrow 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[] \rightarrow 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[] \rightarrow 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[] \rightarrow 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 线段树_结点_03

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

  • 闭区间改开区间,\([1,4]\)改为查询\((0,5)\),扩增至\((M + 0, M + 5) = (8, 13)\);
  • 判断:左端点\(D[1000_B]\)是左儿子,那么其兄弟\(D[1001_B]\)必位于区间内,累加\(ans + = D[1001_B]\)
    判断:右端点\(D[1101_B]\)是右儿子,那么其兄弟\(D[1100_B]\)必位于区间内,累加\(ans + = D[ 1100_B]\)
  • 缩小区间(向根结点缩):$l > > = 1 → 1000 >> 1 = 0100 , r > > = 1 → 1101 > > 1 = 0110 $
  • 判断:左端点\(D[0100_B]\)是左儿子,那么其兄弟\(D[0101_B]\)必位于区间内,累加\(an s + = D[0101_B]\)
    判断:右端点\(D[0110_B]\)是左儿子,不做操作;
  • 缩小区间(向根结点缩):\(l >> = 1 → 0100 > > 1 = 0010 , r > > = 1 → 0110 > > 1 = 0011\)
  • 此时\(l\)和\(r\)同为兄弟,因此终止查询。

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

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

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

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

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

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

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


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

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

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




举报

相关推荐

0 条评论