学习笔记:线段树

线段树

引入

线段树(Segment Tree)几乎是算法竞赛最常用的数据结构了,它主要用于维护区间信息(要求满足结合律)。与树状数组相比,它可以实现 O(logn)区间修改,还可以同时支持多种操作(加、乘),更具通用性。

基础版实现

首先来看一道例题。

【模板】线段树 2

已知一个数列,你需要进行下面三种操作:

  • 将某区间每一个数乘上 x
  • 将某区间每一个数加上 x
  • 求出某区间每一个数的和。

很显然,这道题要用线段树来做,并且需要支持区间加、区间乘、区间求和三种操作。

线段树的建立

线段树是一棵平衡二叉树。母结点代表整个区间的和,越往下区间越小。注意,线段树的每个节点都对应一条线段(区间),但并不保证所有的线段(区间)都是线段树的节点,这两者应当区分开。如果有一个数组 [1,2,3,4,5],那11么它对应的线段树大概长这个样子:

每个节点 node 的左右子节点的编号分别为 node << 1node << 1 | 1,假如节点 node 储存区间 [a,b] 的和,设 mid=(left+right)÷2 ,那么两个子节点分别储存 [l,mid][mid+1,r] 的和。可以发现,左节点对应的区间长度,与右节点相同或者比之恰好多 1

这里的 node << 1node << 1 | 1 实际上分别等价于 node * 2node * 2 + 1,只是采用位运算优化了一些常数而已。

如何从数组建立一棵线段树?我们可以考虑递归地进行。

void pushup(int node){
    tree[node] = tree[node << 1] + tree[node << 1 | 1];tree[node] %= m; // 当前节点权值等于左右子节点权值之和
}
void build(int node, int left, int right){
    mark1[node] = 1; // 首先维护区间乘懒标记
    if(left == right){ // 若到达叶子节点
        tree[node] = a[left];return;
    }
    int mid = left + right >> 1;
    build(node << 1, left, mid); // 求出左子节点
    build(node << 1 | 1, mid + 1, right); // 求出右子节点
    pushup(node); // 维护当前节点权值
}

那么该过程具体是如何实现的呢?还是以 [1,2,3,4,5] 为例:

区间修改

在讲区间修改前,要先引入一个“懒标记”(或者叫做延迟标记)的概念。懒标记是线段树的精髓所在。对于区间修改,朴素的想法是用递归的方式一层层修改(类似于线段树的建立),但这样的时间复杂度比较高。使用懒标记后,对于那些正好是线段树节点的区间,我们不继续递归下去,而是打上一个标记,将来要用到它的子区间的时候,再向下传递。这一道题需要支持区间加、区间乘操作,所以我们需要记录两个懒标记。

更新时,我们是从最大的区间开始,递归向下处理。注意到,任何区间都是线段树上某些节点的并集。于是我们记目标区间为 [l,r] ,当前区间为 [l,r] , 当前节点为 node ,我们会遇到三种情况:

  1. l<r<l<r,即目标区间与当前区间没有交集,此时可以直接结束递归。
  2. ll<rr,即目标区间包含当前区间,此时可以直接更新线段树并打上懒标记。
  3. ll<rrll<rr,即目标区间与当前区间有交集,但不包含,此时需要这时把当前区间一分为二,分别进行处理。(如果存在懒标记,要先把懒标记传递给子节点)。

具体地,对于第一种情况:

       return; // 直接返回

对于第二种情况:

        tree[node] *= k;tree[node] %= m; // 更行当前节点权值
        mark1[node] *= k;mark1[node] %= m; // 维护区间乘懒标记
        mark2[node] *= k;mark2[node] %= m; // 维护区间加懒标记

        tree[node] += k * (right - left + 1);tree[node] %= m; // 更新当前节点权值
        mark2[node] += k;mark2[node] %= m; // 维护区间加懒标记

对于第三种情况:

    pushdown(node, left, right);int mid = left + right >> 1; // 向下传递懒标记
    if(l <= mid)mul(node << 1, left, mid, l, r, k); // 递归更新左子节点
    if(r > mid)mul(node << 1 | 1, mid + 1, right, l, r, k); // 递归更新右子节点
    pushup(node); // 维护当前节点权值

    pushdown(node, left, right);int mid = left + right >> 1; // 向下传递懒标记
    if(l <= mid)add(node << 1, left, mid, l, r, k); // 递归更新左子节点
    if(r > mid)add(node << 1 | 1, mid + 1, right, l, r, k); // 递归更新右子节点
    pushup(node); // 维护当前节点权值

这个过程并不是递归的,我们只往下传递一层(所以叫“懒”标记啊!),以后要用再才继续传递。其实我们常常把这个传递过程封装成一个函数:

void pushdown(int node, int left, int right){
    if(mark1[node] != 1){ // 更新区间乘懒标记
        mark1[node << 1] *= mark1[node];mark1[node << 1] %= m;
        mark1[node << 1 | 1] *= mark1[node];mark1[node << 1 | 1] %= m;
        mark2[node << 1] *= mark1[node];mark2[node << 1] %= m;
        mark2[node << 1 | 1] *= mark1[node];mark2[node << 1 | 1] %= m;
        tree[node << 1] *= mark1[node];tree[node << 1] %= m;
        tree[node << 1 | 1] *= mark1[node];tree[node << 1 | 1] %= m;
        mark1[node] = 1; // 恢复初始状态
    }
    if(mark2[node] != 0){ // 更新区间加懒标记
        int mid = left + right >> 1;
        mark2[node << 1] += mark2[node];mark2[node << 1] %= m;
        mark2[node << 1 | 1] += mark2[node];mark2[node << 1 | 1] %= m;
        tree[node << 1] += mark2[node] * (mid - left + 1);tree[node << 1] %= m;
        tree[node << 1 | 1] += mark2[node] * (right - mid);tree[node << 1 | 1] %= m;
        mark2[node] = 0; // 恢复初始状态
    }
}

传递完标记后,再递归地去处理左右两个子节点。

至于单点修改,只需要令左右端点相等即可。

具体地,对于懒标记的操作,这里采取先乘后加的方法。所谓先乘后加就是在做乘法的时候把加法标记也乘上这个数,在后面做加法的时候直接加就行了。至于证明的话,读者自证不难。

先乘后加可(金)好(坷)啦(垃)好处都有啥?谁说对了就给他!

先乘后加可(金)好(坷)啦(垃),一题能顶两题啦!

先乘后加可(金)好(坷)啦(垃),NOIP一千八!

先乘后加啦,时间不⽩撒;先加后乘呀,撒了也⽩搭

先乘后加可(金)好(坷)啦(垃),不费时~!不怕WA~!

出题人,真不傻!时间给了他,对竞赛体验危害大,绝不能给他!

线段树2毒(不)瘤(发)啊(达),我们都要切(支)掉(援)他。先乘后加,毒(你)瘤(们)数(日)据(本)别~!想~!啦~!

区间查询

依然是差不多的实现原理,这里不再赘述。

int query(int node, int left, int right, int l, int r){
    if(l <= left && r >= right)return tree[node]; // 直接返回
    pushdown(node, left, right);int mid = left + right >> 1;int ans = 0; // 向下传递懒标记
    if(l <= mid)ans += query(node << 1, left, mid, l, r);ans %= m; // 递归求左子节点
    if(r > mid) ans += query(node << 1 | 1, mid + 1, right, l, r);ans %= m; // 递归求右子节点
    return ans; // 返回最终答案
}

一样的递归,一样自顶至底地寻找,一样地合并信息。

这里附上例题的完整代码:

#include <iostream>
#define int long long
#define MAXN 100005
using namespace std;
int n, q, m;
int op, x, y, k;
int a[MAXN];
int tree[MAXN << 2], mark1[MAXN << 2], mark2[MAXN << 2];
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 + '0');
}
void pushup(int node){
    tree[node] = tree[node << 1] + tree[node << 1 | 1];tree[node] %= m; // 当前节点权值等于左右子节点权值之和
}
void pushdown(int node, int left, int right){
    if(mark1[node] != 1){ // 更新区间乘懒标记
        mark1[node << 1] *= mark1[node];mark1[node << 1] %= m;
        mark1[node << 1 | 1] *= mark1[node];mark1[node << 1 | 1] %= m;
        mark2[node << 1] *= mark1[node];mark2[node << 1] %= m;
        mark2[node << 1 | 1] *= mark1[node];mark2[node << 1 | 1] %= m;
        tree[node << 1] *= mark1[node];tree[node << 1] %= m;
        tree[node << 1 | 1] *= mark1[node];tree[node << 1 | 1] %= m;
        mark1[node] = 1; // 恢复初始状态
    }
    if(mark2[node] != 0){ // 更新区间加懒标记
        int mid = left + right >> 1;
        mark2[node << 1] += mark2[node];mark2[node << 1] %= m;
        mark2[node << 1 | 1] += mark2[node];mark2[node << 1 | 1] %= m;
        tree[node << 1] += mark2[node] * (mid - left + 1);tree[node << 1] %= m;
        tree[node << 1 | 1] += mark2[node] * (right - mid);tree[node << 1 | 1] %= m;
        mark2[node] = 0; // 恢复初始状态
    }
}
void build(int node, int left, int right){
    mark1[node] = 1; // 首先维护区间乘懒标记
    if(left == right){ // 若到达叶子节点
        tree[node] = a[left];return;
    }else{
        int mid = left + right >> 1;
        build(node << 1, left, mid); // 求出左子节点
        build(node << 1 | 1, mid + 1, right); // 求出右子节点
        pushup(node); // 维护当前节点权值   
    }
}
void mul(int node, int left, int right, int l, int r, int k){
    if(l <= left && r >= right){
        tree[node] *= k;tree[node] %= m; // 更行当前节点权值
        mark1[node] *= k;mark1[node] %= m; // 维护区间乘懒标记
        mark2[node] *= k;mark2[node] %= m; // 维护区间加懒标记
        return;
    }
    pushdown(node, left, right);int mid = left + right >> 1; // 向下传递懒标记
    if(l <= mid)mul(node << 1, left, mid, l, r, k); // 递归更新左子节点
    if(r > mid)mul(node << 1 | 1, mid + 1, right, l, r, k); // 递归更新右子节点
    pushup(node); // 维护当前节点权值
}
void add(int node, int left, int right, int l, int r, int k){
    if(l <= left && r >= right){
        tree[node] += k * (right - left + 1);tree[node] %= m; // 更新当前节点权值
        mark2[node] += k;mark2[node] %= m; // 维护区间加懒标记
        return;
    }
    pushdown(node, left, right);int mid = left + right >> 1; // 向下传递懒标记
    if(l <= mid)add(node << 1, left, mid, l, r, k); // 递归更新左子节点
    if(r > mid)add(node << 1 | 1, mid + 1, right, l, r, k); // 递归更新右子节点
    pushup(node); // 维护当前节点权值
}
int query(int node, int left, int right, int l, int r){
    if(l <= left && r >= right)return tree[node]; // 直接返回
    pushdown(node, left, right);int mid = left + right >> 1;int ans = 0; // 向下传递懒标记
    if(l <= mid)ans += query(node << 1, left, mid, l, r);ans %= m; // 递归求左子节点
    if(r > mid) ans += query(node << 1 | 1, mid + 1, right, l, r);ans %= m; // 递归求右子节点
    return ans; // 返回最终答案
}
signed main(){
    n = read();q = read();m = read();
    for(int i = 1 ; i <= n ; i ++)a[i] = read();
    build(1, 1, n);
    for(int i = 1 ; i <= q ; i ++){
        op = read();
        switch(op){
            case 1:
                x = read();y = read();k = read();
                mul(1, 1, n, x, y, k);
            case 2:
                x = read();y = read();k = read();
                add(1, 1, n, x, y, k);
            case 3:
                x = read();y = read();
                write(query(1, 1, n, x, y));
                putchar('\n');
        }
    }
    return 0;
}

实际上线段树还可以维护区间最值、区间 gcd 等等,操作除了区间加、区间乘也可以是区间最值、区间赋值,了解原理后很容易改。

当然,线段树还有很多高级应用。

一些问题

如上述代码所示,我们在写线段树的模板时,开4倍的数组就不会溢出了,然而原因是什么呢?

​ 这里给出一些证明。

​ 证明:

​ 首先线段树是一棵二叉树,最底层有 n 个叶子节点(n 为区间大小)。

​ 那么由此可知,此二叉树的高度为 log2n,易证 log2nlog2n+1

​ 由等比数列求和公式 1=1nai=a1(1qn)1q 可知,对于一颗线段树有 1×(12n)12,(n 为树的层数,即树的高度 +1)。

​ 化简得 2log2n+1+11,整理之后近似为 4n(近似计算忽略 1)。

​ 证毕。

权值线段树

权值线段树即一种线段树,以序列的数值为下标。节点里所统计的值为节点所对应的区间 [l,r][l,r] 中,[l,r][l,r] 这个值域中所有数的出现次数。

举个例子,有一个长度为 10 的序列 1,5,2,3,4,1,3,4,4,4

那么统计每个数出现的次数。易知 1 出现了 2 次,2 出现了 1 次,3 出现了 2 次,4 出现了 4 次,5 出现了 1 次。

那么我们可以建出一棵这样的权值线段树:

权值线段树可以在 O(logn) 的时间内求出全局的第 k 小值,某数在全局的排名,一个数的前驱或者后继。因为是线段树的一种所以思想也是二分,其本质就是一个可以二分的桶。

单点更新

单点更新的操作和普通线段树一样,递归到某个叶子节点操作即可。

void update(int node, int left, int right, int pos, int val){
    if(left == right)tree[node] += val;
    else{
        int mid = left + right >> 1;
        if(pos <= mid)update(node << 1, left, mid, pos, val);
        else update(node << 1 | 1, mid + 1, right, pos, val);
        pushup(node);
    }
}

k 小值

这里利用了线段树的天然二分性,如果左子树的值大于等于 k 也就是说 k 小值在左子树,那么就往左子树递归,否则就往右子树递归。

比如查第 5 小,左子树只有 3 个,那么就往右子树递归查询第 2 小。

int kth(int node, int left, int right, int k){
    if(left == right)return left;
    else{
        int mid = left + right >> 1;
        if(tree[node << 1] >= k)return kth(node << 1, left, mid, k);
        else return kth(node << 1 | 1, mid + 1, right, k - tree[node << 1]);
    }
}

求一个数在全局的排名

排名数值上等于全局比那个数小的数的个数 +1,那么放在权值线段树里就等价于前缀和查询,因为节点维护的是区间内出现的数的次数。

例如求 x 的排名,我们只需要查询 [1,x1] 这个区间内的前缀和,然后 +1 就是 x 的排名了。

那也就是普通线段树的区间查询了,只不过左区间恒为 1 罢了。

int query(int node, int left, int right, int l, int r){
    if(l <= left && r >= right)return tree[node];
    else{
        int mid = left + right >> 1, ans = 0;
        if(l <= mid)ans += query(node << 1, left, mid, l, r);
        if(r > mid)ans += query(node << 1 | 1, mid + 1, right, l, r);
        return ans;
    }
}

求前驱

前驱就是小于这个数(令这个数为 node)的最大的数,所以我们优先查小于 x 但尽量大的数,所以当右子树有小于 node 的时候我们优先递归右子树,直到整个区间小于 node,那该区间的右端点就是我们要求的前驱了。

所以我们分两部,找到整个区间刚好小于 node 的节点,再查询那个节点的非空最右子树。

int findpre(int node, int left, int right){
    if(left == right)return left;
    else{
        int mid = left + right >> 1;
        if(tree[node << 1 | 1] != 0)return findpre(node << 1 | 1, mid + 1, right);
        else return findpre(node << 1, left, mid);
    }
}
int getpre(int node, int left, int right, int k){
    if(right < k){
        if(tree[node] != 0)return findpre(node, left, right);
        else return 0;
    }else{
        int mid = left + right >> 1, res;
        if(mid + 1 < k && tree[node << 1 | 1] != 0 && (res = getpre(node << 1 | 1, mid + 1, rihgt, k)))
            return res;
        else return getpre(node << 1, left, mid, k);
    }
}

对于小于 node 的最大的数,我们设 node 的排名为 r(node),则 node 的前驱就是第 r(node)1 小的数,所以我们可以通过 getrank()kth() 结合起来求前驱。

int getpre(int node){
    return kth(1, 1, n, getrank(node) - 1);
}

求后继

后继就是找大于数 node 的最小数,思路和找前驱一样,分两步先找刚好整个区间大于 node 的节点,然后找节点的最左非空子树。

int findnxt(int node, int left, int right){
    if(left == rihgt)return left;
    else{
        int mid = left + right >> 1;
        if(tree[node << 1] != 0)return findnxt(node << 1, left, mid);
        return findnxt(node << 1 | 1, mid + 1, right);
    }
}
int getnxt(int node, int left, int right, int k){
    if(k < left){
        if(tree[node] != 0)return findnxt(node, left, right);
        else return 0;
    }else{
        int mid = left + right >> 1, res;
        if(mid > k && tree[node << 1] != 0 && (res = getnxt(node << 1, left, mid, k)))
            return res;
        else return getnxt(node << 1 | 1, mid + 1, right, k);
    }
}

对于 node 的后继也一样,设 node+1 的排名为 r(node),那么 node 的后继就是第 r(node) 小的数。

int getnxt(int node){
    return kth(1, 1, n, getrank(node + 1)));
}

这里给出模板代码:

#include <iostream>
#include <algorithm>
#define int long long
#define MAXN 100005
using namespace std;
int n, m, op, x;
int tree[MAXN], ans;
void pushup(int node){
    tree[node] = tree[node << 1] + tree[node << 1 | 1];
}
void update(int node, int left, int right, int pos, int val){
    if(left == right)tree[node] += val;
    else{
        int mid = left + right >> 1;
        if(pos <= mid)update(node << 1, left, mid, pos, val);
        else update(node << 1 | 1, mid + 1, right, pos, val);
        pushup(node);
    }
}
int kth(int node, int left, int right, int k){
    if(left == right)return left;
    else{
        int mid = left + right >> 1;
        if(tree[node << 1] >= k)return kth(node << 1, left, mid, k);
        else return kth(node << 1 | 1, mid + 1, right, k - tree[node << 1]);
    }
}
int query(int node, int left, int right, int l, int r){
    if(l <= left && r >= right)return tree[node];
    else{
        int mid = left + right >> 1, ans = 0;
        if(l <= mid)ans += query(node << 1, left, mid, l, r);
        if(r > mid)ans += query(node << 1 | 1, mid + 1, right, l, r);
        return ans;
    }
}
int getrank(int node){
    if(node == 1)return 1;
    else return query(1, 1, n, 1, node - 1) + 1;
}
int findpre(int node, int left, int right){
    if(left == right)return left;
    else{
        int mid = left + right >> 1;
        if(tree[node << 1 | 1] != 0)return findpre(node << 1 | 1, mid + 1, right);
        else return findpre(node << 1, left, mid);
    }
}
int getpre(int node, int left, int right, int k){
    if(right < k){
        if(tree[node] != 0)return findpre(node, left, right);
        else return 0;
    }else{
        int mid = left + right >> 1, res;
        if(mid + 1 < k && tree[node << 1 | 1] != 0 && (res = getpre(node << 1 | 1, mid + 1, right, k)))
            return res;
        else return getpre(noed << 1, left, mid, k);
    }
}
int findnxt(int node, int left, int right){
    if(left == rihgt)return left;
    else{
        int mid = left + right >> 1;
        if(tree[node << 1] != 0)return findnxt(node << 1, left, mid);
        return findnxt(node << 1 | 1, mid + 1, right);
    }
}
int getnxt(int node, int left, int right, int k){
    if(k < left){
        if(tree[node] != 0)return findnxt(node, left, right);
        else return 0;
    }else{
        int mid = left + right >> 1, res;
        if(mid > k && tree[node << 1] != 0 && (res = getnxt(node << 1, left, mid, k)))
            return res;
        else return getnxt(node << 1 | 1, mid + 1, right, k);
    }
}

代替平衡树使用时,权值线段树代码比较短,但是当值域较大、询问较多时空间占用会比较大。权值线段树需要按值域开空间,当值域过大时需要离散化或动态开点。

动态开点

通常来说,线段树占用空间是总区间长 n 的常数倍,空间复杂度是 O(n) 。然而,有时候 n 很巨大,而我们又不需要使用所有的节点,这时便可以动态开点——不再一次性建好树,而是一边修改、查询一边建立。我们不再用 node*2node*2+1 代表左右儿子,而是用 lsrs 记录左右儿子的编号。设总查询次数为 m,则这样的总空间复杂度为 O(mlogn)

比起普通线段树,动态开点线段树有一个优势:它能够处理零或负数位置。此时,求 mid 时不能用 (l+r)/2 ,而要用 (l+r-1)/2(因为 l 等于 r 时会退出递归 ),pushdown 因此也要相应改一下。

因为缓存命中等原因,动态开点线段树写成结构体形式速度往往更快一些。

这里给出前文例题的动态开点做法。

#include <iostream>
#define int long long
#define MAXN 100005
using namespace std;
int n, m, p, op, x, y, k;
int now, tmp, cnt = 1;
struct node{
    int val, mul, add, ls, rs;
}tree[MAXN << 1];
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 ^ 48);
}
void pushup(int &node){
    tree[node].val = tree[tree[node].ls].val + tree[tree[node].rs].val;
}
void pushdown(int node, int left, int right){
    if(tree[node].mul != 1){
        if(tree[node].ls == 0)tree[node].ls = ++cnt,tree[tree[node].ls].mul = 1;
        if(tree[node].rs == 0)tree[node].rs = ++cnt,tree[tree[node].rs].mul = 1;
        tree[tree[node].ls].val *= tree[node].mul;tree[tree[node].ls].val %= p;
        tree[tree[node].rs].val *= tree[node].mul;tree[tree[node].rs].val %= p;
        tree[tree[node].ls].mul *= tree[node].mul;tree[tree[node].ls].mul %= p;
        tree[tree[node].rs].mul *= tree[node].mul;tree[tree[node].rs].mul %= p;
        tree[tree[node].ls].add *= tree[node].mul;tree[tree[node].ls].add %= p;
        tree[tree[node].rs].add *= tree[node].mul;tree[tree[node].rs].add %= p;
        tree[node].mul = 1;
    }
    if(tree[node].add != 0){
        int mid = left + right >> 1;
        if(tree[node].ls == 0)tree[node].ls = ++cnt,tree[tree[node].ls].mul = 1;
        if(tree[node].rs == 0)tree[node].rs = ++cnt,tree[tree[node].rs].mul = 1;
        tree[tree[node].ls].val += tree[node].add * (mid - left + 1);tree[tree[node].ls].val %= p;
        tree[tree[node].rs].val += tree[node].add * (right - mid);tree[tree[node].rs].val %= p;
        tree[tree[node].ls].add += tree[node].add;tree[tree[node].ls].add %= p;
        tree[tree[node].rs].add += tree[node].add;tree[tree[node].rs].add %= p;
        tree[node].add = 0;
    }
}
void build(int &node, int left, int right, int v, int k){
    if(node == 0)node = ++cnt;
    if(left == right)tree[node].val = k;
    else{
        int mid = left + right >> 1;    
        if(v <= mid)build(tree[node].ls, left, mid, v, k);
        else build(tree[node].rs, mid + 1, right, v, k);
        pushup(node);
    }
}
void mul(int &node, int left, int right, int l, int r, int k){
    if(node == 0)node = ++cnt;
    if(l <= left && r >= right){
        tree[node].val *= k;tree[node].val %= p;
        tree[node].mul *= k;tree[node].mul %= p;
        tree[node].add *= k;tree[node].add %= p;
    }else{
        pushdown(node, left, right);int mid = left + right >> 1;
        if(l <= mid)mul(tree[node].ls, left, mid, l, r, k);
        if(r > mid)mul(tree[node].rs, mid + 1, right, l, r, k);
        pushup(node);
    }
}
void add(int &node, int left, int right, int l, int r, int k){
    if(node == 0)node = ++cnt;
    if(l <= left && r >= right){
        tree[node].val += k * (right - left + 1);tree[node].val %= p;
        tree[node].add += k;tree[node].add %= p;
    }else{
        pushdown(node, left, right);int mid = left + right >> 1;
        if(l <= mid)add(tree[node].ls, left, mid, l, r, k);
        if(r > mid)add(tree[node].rs, mid + 1, right, l, r, k);
        pushup(node);
    }
}
int query(int node, int left, int right, int l, int r){
    if(node == 0)return 0;
    else if(l <= left && r >= right)return tree[node].val;
    else{
        pushdown(node, left, right);int mid = left + right >> 1, ans = 0;
        if(l <= mid)ans += query(tree[node].ls, left, mid, l, r),ans %= p;
        if(r > mid)ans += query(tree[node].rs, mid + 1, right, l, r),ans %= p;
        return ans;
    }
}
signed main(){
    n = read();m = read();p = read();
    for(int i = 1 ; i <= n ; i ++)
        tmp = read(),now = 1,build(now, 1, n, i, tmp);
    for(int i = 0 ; i < MAXN << 1 ; i ++)tree[i].mul = 1;
    for(int i = 1 ; i <= m ; i ++){
        op = read();
        switch(op){
            case 1:
                x = read();y = read();k = read();
                now = 1;mul(now, 1, n, x, y, k);
                break;
            case 2:
                x = read();y = read();k = read();
                now = 1;add(now, 1, n, x, y, k);
                break;
            case 3:
                x = read();y = read();
                write(query(1, 1, n, x, y));
                putchar('\n');break;
            default:break;
        }
    }
    return 0;
}

可持久化

大家应该也许或许可能大概用过撤回功能,使用它可以返回之前的某个状态,并在此基础上进行修改。实现这样的功能,便需要可持久化的数据结构,它保存每个历史版本,可以高效地查询和修改历史版本,且因其维持原结构的不变性还在函数式编程领域有很大作用。

可持久化线段树就是这样一种数据结构。想要让线段树可持久化,最朴素的实现方法是每进行一次操作都建一棵新的树,不用说也知道,这样做的时间和空间复杂度都是不可接受的。稍微思考一下会发现,每次修改操作都只会有少数的点被修改,所以大量的点是可以共用的。

要实现这一点,需要我们用动态开点的方法存储每个点的左右子节点。不过,对于最初版本的那棵树,我们应当一次性建好,而不必一个一个插入:

void build(int node, int left, int right){
    if(left == right)tree[node].val = a[left];
    else{
        tree[node].ls = ++cnt;tree[node].rs = ++cnt;
        int mid = left + right >> 1;
        build(tree[node].ls, left, mid);
        build(tree[node].rs, mid + 1, right);
        pushup(node);
    }
}

假如原始数据为 [1,2,3,4,5],现在我们有了形如这样的一棵线段树:

我们维持这棵树不变,添加额外的节点来代替修改操作。

单点修改

加假如当前我们需要将 A42,首先创建一个新的根节点。

要修改的 A4 对应的叶子节点在右子树上。所以,这个新节点的左儿子应该与原节点的左儿子相同(即与之共用左子树)。再创建一个新的节点作为右儿子。

接下来处理这个新节点(这是一个递归的过程)。显然 A4 应该在左子树,所以相似地,沿用原树的右儿子,然后创建一个新的节点作为左儿子。

接下来给各个节点赋值,和普通的线段树类似,叶子节点直接赋值,其它节点则利用子节点计算值。

这样就完成了单点修改。事实上我们没有修改任何东西,我们保留了原来的版本,并新增了一个修改后的版本。

void update(int node, int left, int right, int x, int y, int k){
    if(left == right)tree[x].val = tree[y].val + k;
    else{
        tree[y].ls = tree[x].ls;tree[y].rs = tree[x].rs;
        int mid = left + right >> 1;
        if(node <= mid){
            tree[node].ls = ++cnt;
        	update(node, left, mid, tree[x].ls, tree[y].ls, k);
    	}else{
            tree[node].rs = ++cnt;
        	update(node, mid + 1, right, tree[x].rs, tree[y].rs, k);        	
    	}
        pushup(node);
    }
}

查询操作和普通的查询操作是一样的,只不过需要指定从哪个根节点开始查询。

总结

没有总结,颓疯了。

一上午都在搞颓,开小窗口还被抓了 qwq。

posted @   tsqtsqtsq  阅读(29)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示