• 线段树学习笔记


    什么是线段树

    图片来自百度百科

    线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 (O(log N)) 。而未优化的空间复杂度为 (2N) ,因此有时需要离散化让空间压缩。——by 百度

    怎么构造线段树

    首先明确一件事,根((root))的左孩子是$root cdot 2 (或)root << 1(,右孩子是)root cdot 2+1(或)root<< 1 | 1$

    我们有个大小为 (5) 的数组 (a={10,11,12,13,14}) 要进行区间求和操作,现在我们要怎么把这个数组存到线段树中(也可以说是转化成线段树)呢?我们这样子做:设线段树的根节点编号为 (1) ,用数组 (d) 来保存我们的线段树, (d[i]) 用来保存编号为 (i) 的节点的值(这里节点的值就是这个节点所表示的区间总和),如图所示:

    上图来自(oi-wiki)

    void build(int root,int l,int r) {
        if(l==r) {//如果到了叶子节点就直接赋值
            tree[root]=a[l];
            return;
        }
        int mid=(l+r)/2;
        build(root*2,l,mid);//递归左子树
        build(root*2+1,mid+1,r);//递归右子树
        tree[root]=tree[root*2]+tree[root*2+1];//注意需要更新根节点
    }
    

    我们来看一下(build)函数的运行过程,当从(root)开始递归时递归了左子树,左子树又递归左子树,一直到叶节点返回了左叶节点的值,然后和上面的一样去递归右子树,一直到叶然后返回了右叶节点的值(上面描述的可能不是太清楚,可以自己结合一下上图图),然后一层一层的返回就可以了

    线段树的区间查询

    区间查询,比如求区间 ([l,r]) 的总和(即 (a[l]+a[l+1]+ cdots +a[r]) )、求区间最大值/最小值……还有很多很多……怎么做呢?

    如上图举例
    如果要查询区间 ([1,5]) 的和,那直接获取 (d[1]) 的值( (60) )即可。那如果我就不查询区间 ([1,5]) ,我就查区间 ([3,5]) 呢?

    傻了吧。但其实呢我们肯定还是有办法的!

    你要查的不是 ([3,5]) 吗?我把 ([3,5]) 拆成 ([3,3])([4,5]) 不就行了吗?

    int query(int root,int l,int r,int x,int y) {
        if(x<=l && r<=y) return tree[root];
        int mid=(l+r)/2;
        int ans=0;
        pushdown(root,l,r,mid);
        if(x<=mid)	ans+=query(root*2,l,mid,x,y);
        if(mid<y)   ans+=query(root*2+1,mid+1,r,x,y);
        return ans;
    }
    

    线段树的区间修改与懒惰标记

    这里就是线段树的精髓了,请仔细理解

    区间修改是个很有趣的东西……你想啊,如果你要修改区间 ([l,r]) ,难道把所有包含在区间[l,r]中的节点都遍历一次、修改一次?那估计这时间复杂度估计会上天。这怎么办呢?我们这里要引用一个叫做 「懒惰标记」 的东西。

    我们设一个数组 (b)(b[i]) 表示编号为 (i) 的节点的懒惰标记值。啥是懒惰标记、懒惰标记值呢?这里我再举个例子:

    A 有两个儿子,一个是 B,一个是 C。

    有一天 A 要建一个新房子,没钱。刚好过年嘛,有人要给 B 和 C 红包,两个红包的钱数相同都是 ((1000000000000001mod 2)) 圆(好多啊!……不就是 (1) 元吗……),然而因为 A 是父亲所以红包肯定是先塞给 A 咯~

    理论上来讲 A 应该把两个红包分别给 B 和 C,但是……缺钱嘛,A 就把红包偷偷收到自己口袋里了。

    A 高兴地说:「我现在有 (2) 份红包了!我又多了 (2 imes (1000000000000001mod 2)=2) 元了!哈哈哈~」

    但是 A 知道,如果他不把红包给 B 和 C,那 B 和 C 肯定会不爽然后导致家庭矛盾最后崩溃,所以 A 对儿子 B 和 C 说:「我欠你们每人 (1)((1000000000000001mod 2)) 圆的红包,下次有新红包给过来的时候再给你们!这里我先做下记录……嗯……我钱你们各 ((1000000000000001mod 2)) 圆……」

    儿子 B、C 有点恼怒:「可是如果有同学问起我们我们收到了多少红包咋办?你把我们的红包都收了,我们还怎么装X?」

    父亲 A 赶忙说:「有同学问起来我就会给你们的!我欠条都写好了不会不算话的!」

    这样 B、C 才放了心。

    在这个故事中我们不难看出,A 就是父亲节点,B 和 C 是 A 的儿子节点,而且 B 和 C 是叶子节点,分别对应一个数组中的值(就是之前讲的数组 (a) ),我们假设节点 A 表示区间 ([1,2]) (即 (a[1]+a[2]) ),节点 B 表示区间 ([1,1]) (即 (a[1]) ),节点 C 表示区间 ([2,2]) (即 (a[2]) ),它们的初始值都为 (0) (现在才刚开始呢,还没拿到红包,所以都没钱~)。

    如图:


    注:这里 D 表示当前节点的值(即所表示区间的区间和)。
    为什么节点 A 的 D 是 (2 imes (1000000000000001mod 2)) 呢?原因很简单:节点 A 表示的区间是 ([1,2]) ,一共包含 (2) 个元素。我们是让 ([1,2]) 这个区间的每个元素都加上 (1000000000000001mod 2) ,所以节点 A 的值就加上了 (2 imes (1000000000000001mod 2)) 咯。

    如果这时候我们要查询区间 ([1,1]) (即节点 B 的值)怎么办呢?不是说了吗?如果 B 要用到的时候,A 就把它欠的还给 B!

    具体是这样操作(如图):

    注:为什么是加上 (1 imes (1000000000000001mod 2)) 呢?

    原因和上面一样——B 和 C 表示的区间中只有 (1) 个元素啊!

    由此我们可以得到,区间 ([1,1]) 的区间和就是 (1) 啦!O(∩_∩)O 哈哈~!

    PS:上述解释来自(Oi-wiki),我觉得解释的很好可以看看,附上上面解释的原版代码

    void update(int l, int r, int c, int s, int t,int p){
      // [l,r] 为修改区间,c 为被修改的元素的变化量,[s,t] 为当前节点包含的区间,p 为当前节点的编号
      if (l <= s && t <= r) {
        d[p] += (t - s + 1) * c, b[p] += c;
        return;
      }// 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
      int m = (s + t) / 2; 
      if (b[p] && s!=t){
        // 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
        d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
        b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
        b[p] = 0; // 清空当前节点的标记
      }
      if (l <= m) update(l, r, c, s, m, p * 2);
      if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
      d[p] = d[p * 2] + d[p * 2 + 1];
    }
    

    下面是我的代码:

    区间修改

    void update(int root,int l,int r,int x,int y,int v) {
        if(x<=l && r<=y) return add(root,l,r,v);
        int mid=(l+r)/2;
        pushdown(root,l,r,mid);
        if(x<=mid)	update(root*2,l,mid,x,y,v);
        if(y>mid)   update(root*2+1,mid+1,r,x,y,v);
        tree[root]=tree[root*2]+tree[root*2+1];
    }
    

    (add)函数

    void add(int root,int l,int r,int v) {
        tree[root]+=v*(r-l+1);
        lazy[root]+=v;
    }
    

    下放懒标记

    void pushdown(int root,int l,int r,int mid) {
        if(lazy[root]==0) return ;
        add(root*2,l,mid,lazy[root]);
        add(root*2+1,mid+1,r,lazy[root]);
        lazy[root]=0;
    }
    

    单点修改

    单节点更新是指只更新线段树的某个叶子节点的值,但是更新叶子节点会对其父节点的值产生影响,因此更新子节点后,要回溯更新其父节点的值

    /*
    功能:更新线段树中某个叶子节点的值
    root:当前线段树的根节点下标
    [nstart, nend]: 当前节点所表示的区间
    index: 待更新节点在原始数组arr中的下标
    addVal: 更新的值(原来的值加上addVal)
    */
    void updateOne(int root, int nstart, int nend, int index, int addVal)
    {
        if(nstart == nend)
        {
            if(index == nstart)//找到了相应的节点,更新之
                segTree[root].val += addVal;
            return;
        }
        int mid = (nstart + nend) / 2;
        if(index <= mid)//在左子树中更新
            updateOne(root*2+1, nstart, mid, index, addVal);
        else updateOne(root*2+2, mid+1, nend, index, addVal);//在右子树中更新
        //根据左右子树的值回溯更新当前节点的值
        segTree[root].val = segTree[root*2+1].val+segTree[root*2+2].val;
    }
    

    参考资料

    1.oi-wiki

    2.一步一步理解线段树

    3.信息学奥赛一本通

  • 相关阅读:
    【springcloud alibaba】配置中心之nacos
    【springcloud alibaba】注册中心之nacos
    LeetCode计数质数Swift
    LeetCode移除链表元素Swift
    LeetCode删除重复的电子邮箱SQL
    LeetCode汉明距离Swift
    LeetCode两整数之和Swift
    LeetCode从不订购的客户SQL
    LeetCode超过经理收入的员工SQL
    LeetCode组合两个表SQL
  • 原文地址:https://www.cnblogs.com/pyyyyyy/p/11105650.html
Copyright © 2020-2023  润新知