一些资料下载:线段树学习
猫树
引入
众所周知,线段树的区间查询肥肠的高效,但如果出题人要卡常或者无法快速和并(如线性基:一个最小的数集合,通过异或运算可以表示原集合中任意元素,且集合内部任意元素异或不能得到零。)的情况下,她就会变得较慢。如果题目不需要修改的时候,就有一种神秘的线段树:猫树。
猫树到底是啥
查询区间 [ l , r ] [l,r] [l,r]的时候,它的左右端点的 L C A LCA LCA(最近公共祖先):p代表的区间 [ x , y ] [x,y] [x,y]是一定包含 [ l , r ] [l,r] [l,r]的,且 [ l , r ] [l,r] [l,r]一定经过 [ x , y ] [x,y] [x,y]中点(感性理解一下,如果他没经过 [ x , y ] [x,y] [x,y]中点,那么它应该是另一个子树中的),我们在建树的同时记录一个区间的前缀与后缀,在查询的时候合并 [ l , ( x + y ) / 2 ] 与 ( ( x + y ) / 2 , r ] [l,(x+y)/2]与((x+y)/2,r] [l,(x+y)/2]与((x+y)/2,r]就可以得到答案,预处理后就是O(1)
实现具体流程
定义一个线段树上的节点表示的区间为 ( l , r ] (l,r] (l,r],我们对于每个点多余维护 ( m i d , r ] (mid,r] (mid,r]的前缀和 ( l , m i d ] (l,mid] (l,mid]的后缀,由主定理得时间复杂度为 O ( n log n ) O(n \log n) O(nlogn)。但此时 L C A LCA LCA就变成了时间复杂度瓶颈,考虑将树补全为一颗树,可以发现树上两点的LCA节点为两点的最长公共前缀的十进制表示,有显然LCP(x,y)=x>>(x^y的位数),那么就可已解决了
代码:
建议预处理出x^y的位数,我在实现中偷懒了
本代码实现的是区间求和
cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=300001;
const int LOG=20;
int n,a[N];
int pos[N],st[LOG][N],len[LOG];
void build(int l,int r,int dep){
if(l>=r) return;
int mid=(l+r)>>1;
len[dep]=r-l+1;
st[dep][mid]=a[mid];
for(int i=mid-1;i>=l;i--){
st[dep][i]=st[dep][i+1]+a[i];
}
for(int i=mid+1;i<=r;i++){
st[dep][i]=st[dep][i-1]+a[i];
}
build(l,mid-1,dep+1);
build(mid+1,r,dep+1);
}
void init(){
for(int i=1;i<=n;i++) pos[i]=i;
build(1,n,1);
}
int query(int l,int r){
if(l==r) return a[l];
int dep=32-__builtin_clz(l^r);
return st[dep][l]+st[dep][r];
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
init();
int q;
cin>>q;
while(q--){
int l,r;
cin>>l>>r;
cout<<query(l,r)<<endl;
}
}
ZKW线段树
感谢zkw,让我们拥有了卡常利器
一些性质
让我们来思考一下线段树的复杂度为什么是 O ( n log n ) O(n\log n) O(nlogn),显然每一层中只会被访问最多两个点,因为查询区间是连续的,如果用了两个以上,那么可以合并其中两个区间.
观察线段树上每一个数和它所在的点,发现它们的差为一定值。
正文
ZKW线段树采用完全二叉树的存储方式。对于大小为 n n n 的原始数据,将数组大小扩展为 N = 2 ⌈ log 2 n ⌉ N = 2^{\lceil \log_2 n \rceil} N=2⌈log2n⌉(即大于等于 n n n 的最小二次幂),使得树形结构成为完全二叉树。此时:
叶子节点位于数组的后半部分(下标从 N N N 到 2 N − 1 2N-1 2N−1)
内部节点位于数组的前半部分(下标从 1 1 1 到 N − 1 N-1 N−1)
自底向上构建区间信息
初始化时,先将原始数据填充到叶子节点(数组的后 n n n 个位置)。对于内部节点,从最后一个非叶子节点(下标 N − 1 N-1 N−1)开始向前遍历,每个节点的值通过其左右子节点计算得出。
t r e e [ i ] = t r e e [ 2 i ] ( 你的操作 ) t r e e [ 2 i + 1 ] tree[i] = tree[2i] (你的操作) tree[2i+1] tree[i]=tree[2i](你的操作)tree[2i+1]
对于区间 [ l , r ] [l,r] [l,r]求和的时候,左端点 l 的处理规则:
若 l 对应树中的右子节点,直接累加 a [ l ] a[l] a[l] 并将区间缩小为 ( l , r ] (l, r] (l,r]。
若 l 对应左子节点,将 l 移动到其父节点继续处理。
右端点 r 的处理规则:
若 r 对应树中的左子节点,直接累加 a [ r ] a[r] a[r] 并将区间缩小为 [ l , r ) [l, r) [l,r)。
若 r 对应右子节点,将 r 移动到其父节点继续处理。
区间修改,定位区间左右端点 s 和 t,转换为闭区间 [s, t]。
自底向上处理标记:从根节点到叶子节点的路径上下传标记。
更新区间覆盖的节点值,并标记未完全覆盖的节点的懒标记。
你也可以不上传标记,访问到这个点时将答案加上这个数作为答案,这就是标记永久化,可以用与一般线段树中去,用于优化时间
##代码(区间加和区间求和和区间乘法,下穿标记)
cpp
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
long long n, q, m;
long long tree[1 << 21]; // 空间开2的幂次
long long a[1 << 20];
long long jia[1 << 21], chen[1 << 21];
int M; // ZKW的底层起始位置
void build() {
memset(chen, 1, sizeof(chen));
for (M = 1; M <= n + 1; M <<= 1);
for (int i = M + 1; i <= M + n; i++) {
tree[i] = a[i - M] % m;
}
for (int i = M - 1; i; i--) {
tree[i] = (tree[i << 1] + tree[i << 1 | 1]) % m;
}
}
void down(int u) {
if (u >= M) return;
int lc = u << 1, rc = u << 1 | 1;
tree[lc] = (tree[lc] * chen[u] + jia[u] * (M >> (31 - __builtin_clz(u)))) % m;
tree[rc] = (tree[rc] * chen[u] + jia[u] * (M >> (31 - __builtin_clz(u)))) % m;
chen[lc] = (chen[lc] * chen[u]) % m;
chen[rc] = (chen[rc] * chen[u]) % m;
jia[lc] = (jia[lc] * chen[u] + jia[u]) % m;
jia[rc] = (jia[rc] * chen[u] + jia[u]) % m;
chen[u] = 1;
jia[u] = 0;
}
void chen1(int l, int r, long long c) {
int s = M + l - 1, t = M + r + 1;
for (int i = 31 - __builtin_clz(s ^ t); i >= 0; i--) {
down((s >> i)), down((t >> i));
}
for (; s ^ t ^ 1; s >>= 1, t >>= 1) {
if (~s & 1) {
tree[s ^ 1] = tree[s ^ 1] * c % m;
chen[s ^ 1] = chen[s ^ 1] * c % m;
jia[s ^ 1] = jia[s ^ 1] * c % m;
}
if (t & 1) {
tree[t ^ 1] = tree[t ^ 1] * c % m;
chen[t ^ 1] = chen[t ^ 1] * c % m;
jia[t ^ 1] = jia[t ^ 1] * c % m;
}
}
for (int i = 1; i <= 31 - __builtin_clz(s); i++) {
tree[s >> i] = (tree[s >> i << 1] + tree[s >> i << 1 | 1]) % m;
}
}
void jia1(int l, int r, long long c) {
int s = M + l - 1, t = M + r + 1;
for (int i = 31 - __builtin_clz(s ^ t); i >= 0; i--) {
down((s >> i)), down((t >> i));
}
for (; s ^ t ^ 1; s >>= 1, t >>= 1) {
if (~s & 1) {
tree[s ^ 1] = (tree[s ^ 1] + c * (1 << (31 - __builtin_clz(s ^ 1) - __builtin_clz(M)))) % m;
jia[s ^ 1] = (jia[s ^ 1] + c) % m;
}
if (t & 1) {
tree[t ^ 1] = (tree[t ^ 1] + c * (1 << (31 - __builtin_clz(t ^ 1) - __builtin_clz(M)))) % m;
jia[t ^ 1] = (jia[t ^ 1] + c) % m;
}
}
for (int i = 1; i <= 31 - __builtin_clz(s); i++) {
tree[s >> i] = (tree[s >> i << 1] + tree[s >> i << 1 | 1]) % m;
}
}
long long duodiancha(int l, int r) {
long long ans = 0;
int s = M + l - 1, t = M + r + 1;
for (int i = 31 - __builtin_clz(s ^ t); i >= 0; i--) {
down((s >> i)), down((t >> i));
}
for (; s ^ t ^ 1; s >>= 1, t >>= 1) {
if (~s & 1) ans = (ans + tree[s ^ 1]) % m;
if (t & 1) ans = (ans + tree[t ^ 1]) % m;
}
return ans;
}
int main() {
m = 10007;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
build();
for (int i = 1; i <= n; i++) {
int x;
long long y, k;
cin >> x;
if (x == 1) {
cin >> x >> y >> k;
chen1(x, y, k);
}
else if (x == 0) {
cin >> x >> y >> k;
jia1(x, y, k);
}
else if (x == 2) {
cin >> x >> y >> k;
cout << duodiancha(y, y) % m << endl;
}
}
}
后记
本线段树系列仅仅是浅谈,预计在第三期将
Kinetic Tournament Tree和李超线段树讲完,第四期会选一些杂题来讲