\(zkw\)
参考博文
俗称 重口味 ,与 \(KMP\)
一、\(ZKW\)线段树简介
\(ZKW\)线段树是由清华大学姚班大佬 张昆玮 所创立的一种线段树储存结构,由于其基于非递归的实现方式以及精简的代码和较高的效率而闻名。甚至,\(ZKW\)线段树能够可持久化。
我们从算法的角度对基础线段树进行分析:其实线段树算法本身的本质仍是统计。因此我们可以从统计的角度入手对线段树进行分析:线段树是将一个个数轴划分为区间进行处理的,因此我们面对的往往是一系列的离散量,这导致了我们在使用时的线段树单纯的退化为一棵"点树"(即最底层的线段树只包含一个点)。基于这一点可以入手对线段树进行优化。
二、\(ZKW\)线段树的构造原理
首先,我们忽略线段树中的数据,从线段树的框架结构入手进行分析:如图所示是一颗采用堆式储存的基本线段树:
我们将节点编号转换为二进制:
观察转为二进制后的结点规律:在基础线段树的学习中,我们知道对于任意结点\(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\)线段树中,假设我们要查询区间\([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/29876526https://zhuanlan.zhihu.com/p/29937723
HDU 1166 敌兵布阵 单点更新,区间查询