线段树之zkw
写在前面,本篇博客的内容主要是从洛谷的zkw线段树学习而来,有兴趣的可以看看
https://khong-biet.blog.luogu.org/Introduction-of-zkwSegmentTree
今天学习了一天的zkw线段树,现在说说自己的一点理解吧~
一、zkw线段树简介
-
首先为什么叫 zkw 线段树呢?
大概是因为这种线段树是由 zkw 这个人发明的吧!下面是 zkw 巨佬写的 zkw 线段树:
-
zkw线段树的作用?
简单来说,zkw线段树就是非递归式线段树,平时我们使用的一般线段树都是递归式的线段树。
而一般的线段树的常数有点大,而zkw线段树的常数就比较小了,具体效果的话…看我后面给出那道例题的两种实现方法的差别,虽然不像洛谷文章里面说的那么明显,但是还是有效果的。
因此zkw一般使用在卡常的题目中,不过这样的题目并不常见。
-
zkw线段树与普通线段树的区别?
类型 zkw线段树 线段树 方向 自底向上 自顶向下(再自底向上) 方式 非递归式 递归式 空间 4*maxn 4*maxn 优点 常数小 实现简单,灵活 缺点 不灵活,难懂 常数较大
二、zkw线段树的实现
在实现zkw线段树时有两种方式,按需使用:
① 单点修改+区间查询
② 区间修改+区间查询
前者比较简单,不管是哪种,建树的过程都是一样的,下面的代码都以区间和为例。
-
建树:
总体思想跟普通线段树建树无异,不过是因为是堆式存储,我们可以直接跳到最后一层,确定叶子节点的值之后再逐层向上反馈即可。
那么如何确定最后一层的第一个结点的位置 N 呢?
一般来说 N = 2logn,但是这里为了后续的计算方便令 N = 2log(n+1)。
参考代码:
// 建树 inline void build(int n) { // 计算出最后一层第一个结点的位置 N = 1; for(; N <= n+1; N <<= 1); // 确定叶子节点的值 for(int i = N+1; i <= N+n; i++) scanf("%lld", &tree[i]); // 逐层向上反馈至根结点 // i << 1 即 2*i,表示左孩子 // i << 1 | 1 即 2*i+1,表示右孩子 for(int i = N-1; i >= 1; i--) tree[i] = tree[i<<1]+tree[i<<1|1]; }
-
单点修改+区间查询:
在zkw中单点修改很简单,只要到最后一层找到需要修改的点进行修改,之后逐层向上反馈就可以了!
参考代码:
// 单点修改 inline void change_point(int x, int c) { // N+x 就是最后一层需要修改的点的位置 // 不论当前 x 所在的结点是左孩子还是右孩子,都可以除以 2 来到达父结点 for(x += N; x; x >>= 1) tree[x] += c; }
然后我们需要理解一下在这个方式中区间查询的处理方式了~
先看代码:
// 区间查询 il int ask_interval(int l, int r) { int ans = 0; // 首先把l和r指向最后一层区间 [l, r] 的前一个点和后一个点 // 然后判断当前的结点l和结点r是否同属于一个父亲 // 属于一个父亲的左右孩子结点位置只差1,异或之后 = 1,1^1 = 0,故退出 // 最后逐层向上返回 for(l = N+l-1, r = N+r+1; l^r^1; l >>= 1, r >>= 1) { // 一个数&1可以用来判断奇偶性 // 如果l是左孩子则答案加上右孩子的值,偶数^1相当于加上1 if(~l&1) ans += tree[l^1]; // 如果r是右孩子则答案加上左孩子的值,奇数^1相当于减去1 if(r&1) ans += tree[r^1]; } return ans; }
用洛谷的这张图来讲解一下,
假设我们要求 [2, 6] 的区间和,那么 ans = [2, 2] + [3, 3] + [4, 4] + [5, 5] + [6, 6]
实际上 ans = [2, 3] + [4, 5] + [6, 6] 就可以了,
我们会发现只要当 l 是左孩子的时候加上右孩子的结点,并且当 r 是右孩子的时候加上左孩子的结点,到最后就可以得到 ans 。(l,r 会不断移动到父亲结点)
为什么呢?
个人理解:因为 l 一开始指向的是查询区间的前一个位置,如果这个时候 l 是左孩子的话,那么它的兄弟右孩子必定是处于这个区间中的,因此加上这个值即可;如果 l 是右孩子的话,那么结点 l 的父节点的右边界一定是 l,因此继续对父节点进行上面的判断(右孩子同理),当 l 和 r 同时为一个结点的左右孩子的时候说明其中的所有区间都被考虑完了,可以退出。
(注意查询范围有限,下面是洛谷文章给出的解决方案)
仔细观察上述流程可以发现:我们只能查询
[1,n-1]
范围(这里还是线段树上标的)内的数据如果我们想要查询
[0,m]
范围内( 0\leq m\leq n0≤m≤n )的呢?将数组整体平移!
如果我们想要查询
[m,n]
范围内( 0\leq m\leq n0≤m≤n )的呢?把N直接扩大2倍!
zkw:就是这么狠
-
区间修改+区间查询
这里我们需要采用标记永久化的方法,即lazy标志永远不下传,只在向上反馈的时候加上标记的值。
比如说我们需要在区间 [2, 6] 中每个元素加上 5,
即 [2, 2], [3, 3], [4, 4], [5, 5], [6, 6] 这五个点加上 5,
其实只要在区间 [2, 3], [3, 4] 这两个区间中加上 2*5 = 10,在 [6, 6] 这个点上加上 5 就可以了。
之后再向上反馈,[0, 3] 中加上 2*5 = 10,[4, 7] 中加上 2*5+5 = 3*5 = 15,
最后在 [0, 7] 中加上 2*5 + 3*5 = 5*5 = 25 就完成了。
这中间有两个步骤:① 相遇前修改 ② 相遇后修改
其中 ① 跟上面讲的区间查询类似,也需要借助 l, r 来指向需要修改区间的前一个位置还有后一个位置,根据左右孩子的关系,将区间的值 += 区间中被修改的元素个数 * 修改的值,然后逐级向上返回直到他们同属于一个父亲。
之后就是步骤 ②,这个时候已经计算出这个区间中被修改的元素个数,并且这个区间已经被完整覆盖了,因此只要将父亲区间的值 += 区间中被修改元素的个数*修改的值,无须判断左右孩子,向上返回直到根节点。
参考代码:
// 区间修改 inline void change_interval(int l, int r, LL c) { // lNum和rNum分别保存l和r路上找到的区间被修改元素的数量 // nNum保存当前深度的线段树每个区间的元素个数 int lNum = 0, rNum = 0, nNum = 1; // nNum每次都乘以2 for(l = N+l-1, r = N+r+1; l^r^1; l >>= 1, r >>= 1, nNum <<= 1) { // 不论l和r的左右孩子情况都要加上区间中已修改元素的总变化量 tree[l] += c*lNum; tree[r] += c*rNum; // 根据左右孩子关系对需要修改的区间进行修改,并且修改标记 if(~l&1) {add[l^1] += c; tree[l^1] += c*nNum; lNum += nNum;} if(r&1) {add[r^1] += c; tree[r^1] += c*nNum; rNum += nNum;} } // 相遇之后 for(; l; l >>= 1, r >>= 1) { tree[l] += c*lNum; tree[r] += c*rNum; } }
接下来是这个方式中的区间查询,当然跟上一个方式中的区间查询是不一样的!
主要是因为加入了永久化标记的原因,因此还是需要记录 l 和 r 路上找到的被修改元素个数。
当遇到被标志的区间的时候需要加上对应的值。
参考代码:
// 区间修改 inline LL ask_interval(int l, int r) { // 意义同上 int lNum = 0, rNum = 0, nNum = 1; LL ans = 0; // 注意这里相比于上一个区间查询来说加上了 nNum for(l = N+l-1, r = N+r+1; l^r^1; l >>= 1, r >>= 1, nNum <<= 1) { // 如果存在标记需要处理,处理之 if(add[l]) ans += add[l]*lNum; if(add[r]) ans += add[r]*rNum; // 需要将lNum和rNum加上nNum if(~l&1) {ans += tree[l^1]; lNum += nNum;} if(r&1) {ans += tree[r^1]; rNum += nNum;} } // 需要另外处理相遇之后的情况,直接加即可 for(; l; l >>= 1, r >>= 1) { ans += add[l]*lNum; ans += add[r]*rNum; } return ans; }
三、zkw线段的亲测效果??
似乎没有专门为了zkw线段树而设计的题目,那么我就随便选了一道区间和的题目:
A Simple Problem with Integers POJ - 3468
题目大意什么的就不说了,就是裸的线段树解决区间和的问题,注意使用longlong就可以。
(我的方法丑是丑,但是好歹过了呀…)
首先是使用一般线段树的方法:
然后是zkw线段树:
可以看出来还是有效果的,而且可以看到空间的使用量以及代码长度都有优势。
(当然既然是为了卡常的问题,我在zkw线段树的代码中尽量使用位运算以及inline优化…)
(然而当我在普通线段树的代码中也进行上面的优化之后发现时间甚至到了3157ms??)
下面给出两种方法的代码:
// 普通线段树
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 100000+10;
long long ans;
struct Node {
long long left, right;
long long w;
long long f;
}tree[maxn<<2];
inline void build(int k, long long l, long long r){
tree[k].left = l;
tree[k].right = r;
tree[k].f = 0;
if(l == r){
scanf("%lld", &tree[k].w);
return;
}
int mid = (l+r)>>1;
build(k<<1, l, mid);
build(k<<1|1, mid+1, r);
tree[k].w = tree[k<<1].w+tree[k<<1|1].w;
}
inline void down(int k){
tree[k<<1].f += tree[k].f;
tree[k<<1|1].f += tree[k].f;
tree[k<<1].w += tree[k].f*(tree[k<<1].right-tree[k<<1].left+1);
tree[k<<1|1].w += tree[k].f*(tree[k<<1|1].right-tree[k<<1|1].left+1);
tree[k].f = 0;
}
inline void ask_interval(int k, long long a, long long b){
if(tree[k].left >= a && tree[k].right <= b){
ans += tree[k].w;
return;
}
if(tree[k].f) down(k);
int mid = (tree[k].left+tree[k].right)>>1;
if(a <= mid) ask_interval(k<<1, a, b);
if(b > mid) ask_interval(k<<1|1, a, b);
}
inline void change_interval(int k, long long a, long long b, long long c){
if(tree[k].left >= a && tree[k].right <= b){
tree[k].w += c*(tree[k].right-tree[k].left+1);
tree[k].f += c;
return;
}
if(tree[k].f) down(k);
int mid = (tree[k].left+tree[k].right)>>1;
if(a <= mid) change_interval(k<<1, a, b, c);
if(b > mid) change_interval(k<<1|1, a, b, c);
tree[k].w = tree[k<<1].w+tree[k<<1|1].w;
}
int main(){
int N, Q;
while(scanf("%d%d", &N, &Q) == 2){
build(1, 1, N);
for(int i = 0; i < Q; i++){
getchar();
char ch;
scanf("%c", &ch);
if(ch == 'Q'){
ans = 0;
long long a, b;
scanf("%lld%lld", &a, &b);
ask_interval(1, a, b);
printf("%lld\n", ans);
}
else{
long long a, b;
long long c;
scanf("%lld%lld%lld", &a, &b, &c);
change_interval(1, a, b, c);
}
}
}
return 0;
}
// zkw线段树
#include<iostream>
#include<cstdio>
#include<cstring>
#define LL long long
using namespace std;
const int maxn = 1e5+10;
int n, N, m;
LL tree[maxn<<2], add[maxn<<2];
inline void build(int n) {
N = 1;
for(; N <= n+1; N <<= 1);
for(int i = N+1; i <= N+n; i++) scanf("%lld", &tree[i]);
for(int i = N-1; i >= 1; i--) tree[i] = tree[i<<1]+tree[i<<1|1];
}
inline void change_interval(int l, int r, LL c) {
int lNum = 0, rNum = 0, nNum = 1;
for(l = N+l-1, r = N+r+1; l^r^1; l >>= 1, r >>= 1, nNum <<= 1) {
tree[l] += c*lNum;
tree[r] += c*rNum;
if(~l&1) {add[l^1] += c; tree[l^1] += c*nNum; lNum += nNum;}
if(r&1) {add[r^1] += c; tree[r^1] += c*nNum; rNum += nNum;}
}
for(; l; l >>= 1, r >>= 1) {
tree[l] += c*lNum;
tree[r] += c*rNum;
}
}
inline LL ask_interval(int l, int r) {
int lNum = 0, rNum = 0, nNum = 1;
LL ans = 0;
for(l = N+l-1, r = N+r+1; l^r^1; l >>= 1, r >>= 1, nNum <<= 1) {
if(add[l]) ans += add[l]*lNum;
if(add[r]) ans += add[r]*rNum;
if(~l&1) {ans += tree[l^1]; lNum += nNum;}
if(r&1) {ans += tree[r^1]; rNum += nNum;}
}
for(; l; l >>= 1, r >>= 1) {
ans += add[l]*lNum;
ans += add[r]*rNum;
}
return ans;
}
int main() {
while(~scanf("%d%d", &n, &m)) {
memset(add, 0, sizeof(add));
build(n);
for(int i = 0; i < m; i++) {
char ch[3];
scanf("%s", ch);
int l, r;
scanf("%d%d", &l, &r);
if(ch[0] == 'Q') printf("%lld\n", ask_interval(l, r));
else {
LL c;
scanf("%lld", &c);
change_interval(l, r, c);
}
}
}
return 0;
}
【END】感谢观看!