• 线段树学习笔记


    写在前言之前

    很早之前我就开始学最基本的线段树操作,最高的只到区间加法。时隔多日才想起来在深入一下,加之很久没有写过线段树的板子,所以写的时候还是比较恶心的

    这次主攻的是区间乘法的操作。

    前言

    对于区间查询这一类问题。如果给定的是一个有序的序列,完全可以使用前缀和求解。求解无序的区间查询是比较常用的有ST表和线段树,今天要说的便是线段树这一数据结构

    0x00

    线段树是一棵二叉搜索树,每一个节点都储存有一些信息,通过对这些信息的修改和维护可以做到$O(nlogn)$的时间内建树$+$修改$+$查询。

    可能下面这张图能够更加直观的解释线段树是啥

    每个点下的红色的字体表示区间的左右端点,每个点里面的数是这个店所代表的区间的和,最下面黄色的店里面的是序列中的元素

    这张图这么好看,怎么可能是我画的呢QAQ

    线段树版本1

    0x01

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

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

    在这种情况下,我们要用到最普通的线段树,支持区间加法和区间查询。

    线段树将树上的节点都看做一条线段,每个节点上都维护着一些信息

    如果上面的题目的话,就需要维护下列的信息

    1. $l$和$r$表示区间(线段)的左端点和右端点
    2. $sum$表示这个区间的元素的总和
    3. $lazytag$,先记住,这东西叫做懒标记,在后面会作出解释

    本篇文章全部使用数组来实现,指针党请谅解

    0x02

    我们先来看如何建立一棵线段树。通过一个$build$函数来实现,这个函数的时间复杂度是$O(nlogn)$

    首先从根节点开始向下扩展,显然根节点存储的$l$和$r$应该是$l=1,r=n$。每次扩展时计算一个$mid$值$=(l+r)/2$

    这个从$l$到$mid$这段区间放到左儿子中,$mid+1$到$r$放到右儿子里。

    如果搜索到$l=r$时。已经到底了,这个区间的值就可以确定了,是第$l$个元素。

    然后就可以回溯。在回溯的时候维护区间和。另根节点的$sum$等于左右儿子的$sum$的和。

    代码如下

    inline void build(int k, int ll, int rr) {
        //k是节点的编号,ll是该节点的左端点,rr是右端点 
        tree[k].l = ll, tree[k].r = rr;
        //赋值 
        if(tree[k].l == tree[k].r) {
            //如果找到了l等于r的情况证明已经到了最底部。可以直接输入 
            scanf("%lld", &tree[k].w);
            return ;
            //回溯 
        }
        long long int mid = (ll+rr)/2;
        build((k<<1), ll, mid); //建立左儿子 
        build((k<<1)+1, mid+1, rr); //建立右儿子 
        tree[k].w = tree[(k<<1)].w+tree[(k<<1)+1].w;
        //维护区间和 
    }

    为了方便大家理解我还录制了GIF给大家看看

    0x03

    再来说区间加法,这里引入一个前文提到过的概念-----懒标记

    顾名思义,懒标记的作用就是懒。它要怎么懒呢?

    在进行区间修改的时候我们要减少多余的操作。将一个区间全都加上一个数时。我们只对要用到的区间进行操作。对于那些之后要用到的但现在没用到的区间我们可以先不修改,用$lazytag$存储一个值,什么时候用到什么时候下传给儿子,在一步步下传到要用到的区间。

    这个操作用一个$down$函数来实现

    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)].r-tree[(k<<1)].l+1);
        tree[(k<<1)+1].w += tree[k].f*(tree[(k<<1)+1].r-tree[(k<<1)+1].l+1);
        //更新左右儿子的区间和 
        tree[k].f = 0;
        //清除父亲结点的懒标记 
    }

      

    然后这个区间加法就只剩下普通的操作了,看下面的区间修改代码

    inline void change_interval(int k) {
        if(tree[k].l >= a&&tree[k].r <= b) {
            tree[k].w += (tree[k].r-tree[k].l+1)*y;
            tree[k].f += y;
            //更新当前区间的和还有当前区间的懒标记 
            return ;
        }
        if(tree[k].f) down(k);
        //如果懒标记不为0的话就下传给自己的儿子 
        int mid = (tree[k].l+tree[k].r)/2;
        if(a <= mid) change_interval((k<<1));
        if(b > mid) change_interval((k<<1)+1);
        tree[k].w = tree[(k<<1)].w+tree[(k<<1)+1].w;
        //维护区间和 
    }

      

    0x04

    至于区间查询,和区间修改是差不多的

    inline void ask_interval(int k) {
        //如果当前的区间被要查询的区间包含的话,直接加到答案中 
        if(tree[k].l >= a&&tree[k].r <= b) {
            ans += tree[k].w;
            return ;
        }
        if(tree[k].f) down(k);
        int mid = (tree[k].l+tree[k].r)/2;
        //判断左右儿子的区间和要查询的区间是否有交集 
        if(a <= mid) ask_interval((k<<1));
        if(b > mid) ask_interval((k<<1)+1);
    }

    0x05

    下面放上我的完整的代码

    #include <iostream>
    #include <cstdio>
    
    using namespace std;
    
    struct node{
        int l, r;
        long long w, f;                        //(l, r)区间,区间和w,懒标记f;
    }tree[400001];
    long long int ans, y;
    int x, n, m;
    int a, b;
    
    inline void build_tree(int k, int ll, int rr) {
        tree[k].l = ll, tree[k].r = rr;
        if(tree[k].l == tree[k].r) {
            scanf("%lld", &tree[k].w);
            return ;
        }
        long long int mid = (ll+rr)/2;
        build_tree((k<<1), ll, mid);
        build_tree((k<<1)+1, mid+1, rr);
        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)].r-tree[(k<<1)].l+1);
        tree[(k<<1)+1].w += tree[k].f*(tree[(k<<1)+1].r-tree[(k<<1)+1].l+1);
        tree[k].f = 0;
    }
    
    inline void ask_point(int k) {
        if(tree[k].l == tree[k].r) {
            ans = tree[k].w;
            return ;
        }
        if(tree[k].f) down(k);
        int mid = (tree[k].l+tree[k].r)/2;
        if(x <= mid) ask_point((k<<1));
        else ask_point((k<<1)+1);
    }
    
    inline void change_point(int k) {
        if(tree[k].l == tree[k].r) {
            tree[k].w += y;
            return ;
        }
        if(tree[k].f) down(k);
        int mid = (tree[k].l+tree[k].r)/2;
        if(x <= mid) change_point((k<<1));
        else change_point((k<<1)+1);
        tree[k].w = tree[(k<<1)].w+tree[(k<<1)+1].w;
    }
    
    inline void ask_interval(int k) {
        if(tree[k].l >= a&&tree[k].r <= b) {
            ans += tree[k].w;
            return ;
        }
        if(tree[k].f) down(k);
        int mid = (tree[k].l+tree[k].r)/2;
        if(a <= mid) ask_interval((k<<1));
        if(b > mid) ask_interval((k<<1)+1);
        // tree[k].w = tree[(k<<1)].w+tree[(k<<1)+1].w;
    }
    
    inline void change_interval(int k) {
        if(tree[k].l >= a&&tree[k].r <= b) {
            tree[k].w += (tree[k].r-tree[k].l+1)*y;
            tree[k].f += y;
            return ;
        }
        if(tree[k].f) down(k);
        int mid = (tree[k].l+tree[k].r)/2;
        if(a <= mid) change_interval((k<<1));
        if(b > mid) change_interval((k<<1)+1);
        tree[k].w = tree[(k<<1)].w+tree[(k<<1)+1].w;
    }
    
    int main() {
        scanf("%d%d", &n, &m);
        build_tree(1, 1, n);
        for(int i=1; i<=m; i++) {
            int p;
            ans = 0;
            scanf("%d", &p);
            switch(p) {
                /*ask_point*/case 4: {
                    scanf("%d", &x);
                    ask_point(1);
                    printf("%lld
    ", ans);
                    break;
                }
                /*change_point*/case 3: {
                    scanf("%d%d", &x, &y);
                    change_point(1);
                    break;
                }
                /*ask_interval*/case 2: {
                    scanf("%d%d", &a, &b);
                    ask_interval(1);
                    printf("%lld
    ", ans);
                    break;
                }
                /*change_interval*/case 1: {
                    scanf("%d%d%lld", &a, &b, &y);
                    change_interval(1);
                    break;
                }
            }
        }
        return 0;
    }

    0x06

    来几个例题

    线段树版本2

    0x00

    这个版本的线段树呢,就是加入了更多的区间操作。比如区间乘法,但这些操作大致相同,这里以区间乘法为例进行讲解

    像上面的加法有加法标记一样,乘法也有乘法标记。

    0x01

    建树的过程与版本一的建树过程大致相同

    这里不再进行详细讲解,只放上代码。唯一要值得注意的是懒标记的初始化,乘法标记要初始化为$1$。

    inline void build(int k, int ll, int rr) {
        tree[k].l = ll, tree[k].r = rr;
        tree[k].addtag = 0, tree[k].multag = 1;
        if(tree[k].l == tree[k].r) {
            tree[k].sum = read();
            tree[k].sum %= Mod;
            return ;
        }
        int mid = (tree[k].l + tree[k].r) >> 1;
        build(Lson, tree[k].l, mid);
        build(Rson, mid + 1, tree[k].r);
        tree[k].sum = tree[Lson].sum + tree[Rson].sum;
        tree[k].sum %= Mod;
    }

      

    0x02

    懒标记的下传是整个线段树中最核心的部分,一般情况下如果你写的线段树WA掉了,那肯定是你写的懒标记下传函数出了锅

    带有区间加法的线段树的下传函数非常复杂。通常情况下我们先下传乘法标记,在下传加法标记。因为先进行乘法,对之后的加法不会产生什么影响,如果先进行加法的话,对之后的乘法就会产生影响。所以我们选择先进行乘法标记的下传。在下传加法标记的同时直接将乘法标记也下传给加法标记。

    下面给出$down$函数的代码

    inline void pushdown(int k) {
        tree[Lson].multag = tree[k].multag * tree[Lson].multag % Mod;
        tree[Rson].multag = tree[k].multag * tree[Rson].multag % Mod;
        tree[Lson].addtag = tree[Lson].addtag * tree[k].multag % Mod;
        tree[Rson].addtag = tree[Rson].addtag * tree[k].multag % Mod;
        tree[Lson].sum = tree[Lson].sum * tree[k].multag % Mod;
        tree[Rson].sum = tree[Rson].sum * tree[k].multag % Mod;
        tree[Lson].addtag = (tree[k].addtag + tree[Lson].addtag) % Mod;
        tree[Rson].addtag = (tree[k].addtag + tree[Rson].addtag) % Mod;
        int L = (tree[Lson].r - tree[Lson].l + 1);
        int R = (tree[Rson].r - tree[Rson].l + 1);
        tree[Lson].sum = (tree[Lson].sum + L * tree[k].addtag) % Mod;
        tree[Rson].sum = (tree[Rson].sum + R * tree[k].addtag) % Mod;
        tree[k].addtag = 0, tree[k].multag = 1;
    }

      

    0x03

    区间乘法和区间加法的更新都和线段树版本1异曲同工

    所以不再进行讲解

    直接给出代码

    区间加法更新

    inline void update_add(int k) {
        if(tree[k].l >= x && tree[k].r <= y) {
            tree[k].sum = (tree[k].sum + (tree[k].r - tree[k].l + 1) * z) % Mod;
            tree[k].addtag = (tree[k].addtag + z) % Mod;
            return ;
        }
        pushdown(k);
        int mid = (tree[k].l + tree[k].r) >> 1;
        if(mid >= x) update_add(Lson);
        if(mid < y) update_add(Rson);
        tree[k].sum = (tree[Lson].sum + tree[Rson].sum) % Mod;
    }

      

    区间乘法更新

    inline void update_mul(int k) {
        if(tree[k].l >= x && tree[k].r <= y) {
            tree[k].sum = tree[k].sum * z % Mod;
            tree[k].addtag = tree[k].addtag * z % Mod;
            tree[k].multag = tree[k].multag * z % Mod;
            return ;
        }
        pushdown(k);
        int mid = (tree[k].l + tree[k].r) >> 1;
        if(mid >= x) update_mul(Lson);
        if(mid < y) update_mul(Rson);
        tree[k].sum = (tree[Lson].sum + tree[Rson].sum) % Mod;
    }

    0x04

    还是放上完整的代码

    #include <iostream>
    #include <cstdio>
    #define Lson (k << 1)
    #define Rson (k << 1) + 1
    
    typedef long long LL;
    const int maxn = 4e5+3;
    LL n, m, Mod, x, y, z, c;
    struct node {
        LL l, r, sum, addtag, multag;
    }tree[maxn];
    LL xx, f; char ch;
    inline LL read() {
        xx = 0, f = 1; ch = getchar();
        while (ch < '0' || ch > '9') {
            if(ch == '-') f = -1;
            ch = getchar();
        }
        while (ch <= '9' && ch >= '0') {
            xx = xx * 10 + ch - '0';
            ch = getchar();
        }
        return xx * f;
    }
    inline void build(int k, int ll, int rr) {
        tree[k].l = ll, tree[k].r = rr;
        tree[k].addtag = 0, tree[k].multag = 1;
        if(tree[k].l == tree[k].r) {
            tree[k].sum = read();
            tree[k].sum %= Mod;
            return ;
        }
        int mid = (tree[k].l + tree[k].r) >> 1;
        build(Lson, tree[k].l, mid);
        build(Rson, mid + 1, tree[k].r);
        tree[k].sum = tree[Lson].sum + tree[Rson].sum;
        tree[k].sum %= Mod;
    }
    inline void pushdown(int k) {
        tree[Lson].multag = tree[k].multag * tree[Lson].multag % Mod;
        tree[Rson].multag = tree[k].multag * tree[Rson].multag % Mod;
        tree[Lson].addtag = tree[Lson].addtag * tree[k].multag % Mod;
        tree[Rson].addtag = tree[Rson].addtag * tree[k].multag % Mod;
        tree[Lson].sum = tree[Lson].sum * tree[k].multag % Mod;
        tree[Rson].sum = tree[Rson].sum * tree[k].multag % Mod;
        tree[Lson].addtag = (tree[k].addtag + tree[Lson].addtag) % Mod;
        tree[Rson].addtag = (tree[k].addtag + tree[Rson].addtag) % Mod;
        int L = (tree[Lson].r - tree[Lson].l + 1);
        int R = (tree[Rson].r - tree[Rson].l + 1);
        tree[Lson].sum = (tree[Lson].sum + L * tree[k].addtag) % Mod;
        tree[Rson].sum = (tree[Rson].sum + R * tree[k].addtag) % Mod;
        tree[k].addtag = 0, tree[k].multag = 1;
    }
    inline void update_mul(int k) {
        if(tree[k].l >= x && tree[k].r <= y) {
            tree[k].sum = tree[k].sum * z % Mod;
            tree[k].addtag = tree[k].addtag * z % Mod;
            tree[k].multag = tree[k].multag * z % Mod;
            return ;
        }
        pushdown(k);
        int mid = (tree[k].l + tree[k].r) >> 1;
        if(mid >= x) update_mul(Lson);
        if(mid < y) update_mul(Rson);
        tree[k].sum = (tree[Lson].sum + tree[Rson].sum) % Mod;
    }
    inline void update_add(int k) {
        if(tree[k].l >= x && tree[k].r <= y) {
            tree[k].sum = (tree[k].sum + (tree[k].r - tree[k].l + 1) * z) % Mod;
            tree[k].addtag = (tree[k].addtag + z) % Mod;
            return ;
        }
        pushdown(k);
        int mid = (tree[k].l + tree[k].r) >> 1;
        if(mid >= x) update_add(Lson);
        if(mid < y) update_add(Rson);
        tree[k].sum = (tree[Lson].sum + tree[Rson].sum) % Mod;
    }
    inline LL check(int k) {
        if(tree[k].l > y || tree[k].r < x) return 0;
        if(tree[k].l >= x && tree[k].r <= y) {
            return tree[k].sum % Mod;
        }
        pushdown(k);
        int mid = (tree[k].l + tree[k].r) >> 1;
        return (check(Lson) + check(Rson)) % Mod;
    }
    
    int main() {
        n = read(), m = read(), Mod = read();
        build(1, 1, n);
        for(int i=1; i<=m; i++) {
            c = read();
            switch(c) {
                case 1:
                    x = read(), y = read(), z = read();
                    update_mul(1);
                    break;
                case 2:
                    x = read(), y = read(), z = read();
                    update_add(1);
                    break;
                case 3:
                    x = read(), y = read();
                    printf("%lld
    ", check(1));
            }
        }
    }

    0x05

    还有很多其他类型的线段树,比如超哥线段树、吉司机线段树、zkw线段树什么的。这里不再深入讲解。

    感兴趣的同学可以自行百度

  • 相关阅读:
    Vue学习-Day1
    Shell脚本学习
    Linux基础
    C# int.Parse()、int.TryParse()与Convert.ToInt32()的区别
    windows下跑python flask,环境配置
    linq性能剖析
    servicestack操作redis
    程序员福利各大平台免费接口非常适用
    ASP.NET下跨应用共享Session和使用Redis进行Session托管简介
    Web Farm和Web Garden的区别
  • 原文地址:https://www.cnblogs.com/bljfy/p/9431558.html
Copyright © 2020-2023  润新知