ZKW线段树
今天终于把ZKW线段树搞出来了,也算还了一个欠账。
首先,我相信大家都会常规的线段树,也就是递归版的线段树,并且,也会简单的树状数组。
我们知道,树状数组和线段树时间复杂度都是(O(nlog_n)),但是树状数组要快上不少,究其根本,是因为树状数组在实现上是非递归,而常规线段树是递归的。所以我们考虑使用非递归手段来实现线段树,这就是ZKW线段树。
要实现非递归,自上而下很难想出来,所以我们可以考虑自下而上地实现线段树。
按照线段树的建树方法,我们可以发现,节点 u 的父亲节点就是 u>>1, 而它的兄弟节点则为 u^1。
我们先把要处理的区间(无论是修改还是查询)由闭区间改为开区间。这样如果区间的左端点是其父亲的左儿子(~l & 1), 那么其兄弟节点必定在待处理的区间内部,同理,如果右端点是其父亲节点的右儿子(r & 1), 那么其兄弟节点也必定在待处理区间内。当左端点和右端点是兄弟节点时(l ^ r ^ 1),则处理结束。
以上这些判断都可以使用位运算来加速。
以上这些,看一看,想一想就足以用来解决单点修改区间查询一类问题。对于区间修改单点查询这一类问题,用差分也可以很快搞出来。
但是,在处理区间修改区间查询这一类问题时,和常规线段树相同,我们需要维护lazy标记。但是,怎么下放标记? 答案是不用下放,直接永久化标记!!!
但是此处有一个坑点,如果标记在上面,而查询时在标记下方就结束,那么答案就会少算,怎么办?那就一直更新到根节点,用lazy标记乘上子节点个数即可。
下面用一道区间修改区间查询求和的裸题来作为例题,还不懂的可以看代码来帮助理解
codevs1082
代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int maxn = 200000 + 5;
int n, M;
LL sum[maxn << 2], add[maxn << 2];
void build() {
M = 1;
while(M < (n + 2)) M <<= 1;
for (int i = 1; i <= n; i++) {
int x = M + i;
scanf("%lld", sum + x);
while(x >>= 1) sum[x] = sum[x << 1] + sum[x << 1 | 1];
}
}
void update(int l, int r, LL x) {
l += M - 1, r += M + 1;
int L = 0, R = 0;
for (int i = 1; l ^ r ^ 1; i <<= 1, l >>= 1, r >>= 1) {
sum[l] += L * x, sum[r] += R * x;
if(~l & 1) add[l ^ 1] += x, sum[l ^ 1] += i * x, L += i;
if(r & 1) add[r ^ 1] += x, sum[r ^ 1] += i * x, R += i;
}
sum[l] += L * x, sum[r] += R * x;
while(l >>= 1) sum[l] += x * (L + R);
}
LL query(int l, int r) {
l += M - 1, r += M + 1;
LL ans = 0;
int L = 0, R = 0;
for (int i = 1; l ^ r ^ 1; i <<= 1, l >>= 1, r >>= 1) {
ans += add[l] * L + add[r] * R;
if(~l & 1) ans += sum[l ^ 1], L += i;
if(r & 1) ans += sum[r ^ 1], R += i;
}
ans += add[l] * L + add[r] * R;
while(l >>= 1) ans += add[l] * (L + R);
return ans;
}
int main() {
scanf("%d", &n);
build();
int q;
scanf("%d", &q);
while(q--) {
int op, a, b, c;
scanf("%d%d%d", &op, &a, &b);
if(op == 1) {
scanf("%d", &c);
update(a, b, c);
}
else
printf("%lld
", query(a, b));
}
return 0;
}