HAO的线段树(中(上))

一些资料下载:线段树学习

猫树

引入

众所周知,线段树的区间查询肥肠的高效,但如果出题人要卡常或者无法快速和并(如线性基:一个最小的数集合,通过异或运算可以表示原集合中任意元素,且集合内部任意元素异或不能得到零。)的情况下,她就会变得较慢。如果题目不需要修改的时候,就有一种神秘的线段树:猫树。

猫树到底是啥

查询区间 [ 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和李超线段树讲完,第四期会选一些杂题来讲

相关推荐
LYS_06181 小时前
C++学习(5)(函数 指针 引用)
java·c++·算法
紫陌涵光2 小时前
669. 修剪二叉搜索树
算法·leetcode
NGC_66112 小时前
二分查找算法
java·javascript·算法
ADDDDDD_Trouvaille2 小时前
2026.2.21——OJ95-97题
c++·算法
blackicexs2 小时前
第五周第七天
数据结构·算法
夏乌_Wx3 小时前
反转链表:三种实现思路与细节梳理
数据结构·链表
Once_day3 小时前
C++之《程序员自我修养》读书总结(4)
c语言·c++·编译和链接
近津薪荼3 小时前
dfs专题10——全排列 II
算法·深度优先