线段树:区间树:逻辑上的二分法,以空间换时间完成一段区间上的add,update,query任务

线段树:区间树:逻辑上的二分法,以空间换时间完成一段区间上的add,update,query任务

提示:线段树是非常非常强大的优化技能,难,但是捋清逻辑,就不难了

重要的基础知识:
【1】堆,系统堆结构,手动改写堆结构,堆结构远比比堆排序更重要


题目

问题引入:请你设计一个数据结构,搞定下面的几个功能。

给你一个数组arr,请你在给定的L–R这段区间范围上的完成这三种任务:

【1】add(L,R,C),在arr的L–R区间上整体每一个元素都 C,
【2】update(L,R,C),把arr的L–R区间上整体每一个元素都更新为C,
【3】query(L,R),请返回arr的L–R区间上整体每一个元素累加和


笔试AC:暴力解理解题目

笔试也不可能考这么简单的题目,这都是为面试准备的!

暴力解o(R-L)速度即,o(n)速度遍历搞,简单而高效
简单

给你一个数组
arr=1 1 1 1 1
i =0 1 2 3 4

记住,为了方便你理解,咱们不用0下标
新搞一个数组把arr向右挪一个位置
arr=1 1 1 1 1
i =1 2 3 4 5

设计一个暴力数据结构Right:
不妨设
【1】add(1,2,2),在arr的L–R区间上整体每一个元素都 2,
之后arr=3 3 1 1 1

【2】update(1,5,5),把arr的L–R区间上整体每一个元素都更新为5,
之后arr=5 5 5 5 5

【3】query(1,5),请返回arr的L–R区间上整体每一个元素累加和
返回ans= 5*5=25

直接用arr暴力遍历搞定

    //首先,笔试中,直接暴力解,o(N^2)复杂度
    public static class Right{
        public int[] arr;//拿来重新搞数组,不要0坐标
        public Right(int[] origin){
            //初始化,Right结构
            arr = new int[origin.length + 1];//咱不用0下标
            for (int i = 0; i < origin.length; i++) {
                arr[i + 1] = origin[i];
            }
        }

        //add任务,L--R,全部加一个C
        public void add(int L, int R, int C){
            for (int i = L; i <= R; i++) {
                arr[i] += C;
            }
        }
        //update, L--R,全部更新为C
        public void update(int L, int R, int C){
            for (int i = L; i <= R; i++) {
                arr[i] = C;
            }
        }
        //query,  L--R,查询这个范围的累加和
        public int query(int L, int R){
            int res = 0;
            for (int i = L; i <= R; i++) {
                res += arr[i];
            }
            return res;
        }
        //非常非常容易,但是复杂度为O(N)
    }

    public static void test(){
        int[] origin = {1,1,1,1,1};
        Right right = new Right(origin);
        right.add(1, 2, 2);//1--2上全部加一个2
        for (int i = 1; i < right.arr.length; i++) {
            System.out.print(right.arr[i] +" ");
        }
        System.out.println();
        right.update(1, 5, 5);//1--5上全部更新为2
        for (int i = 1; i < right.arr.length; i++) {
            System.out.print(right.arr[i] +" ");
        }
        System.out.println();
        int res = right.query(1, 5);//1--5上所有元素累加和
        System.out.println(res);
    }

结果:

3 3 1 1 1 
5 5 5 5 5 
25

线段树:区间数,利用逻辑上的二分法把arr转化为二叉树

咱们这么想:
你暴力遍历需要o(n)速度,咱可否利用额外空间,预存一些范围内的累加和信息呢

说白了,打表搞一个sum数组,直接一开始,就把所有1–N范围内的任意l–r范围的累加和放到sum里面
方便来一个L–R查询累加和时,咱们直接二分去查这个sum
l–r范围内的信息,咱放sum的一个下标i里面,这样简洁一些,满二叉树可以搞定这个对应关系
在这里插入图片描述
上图中
5 6 7 8下标分别代表arr的1 2 3 4位置元素,和就是1 1 1 1
2下标代表1–2范围内的累加和,就是5 6 节点的和2
3下标代表3–4范围内的累加和,就是7 8 节点的和2
1下标代表1–4范围的累加和,就是2 3节点的和4

那么,反过来看,1–4范围内二分,为1-2,3-4,再二分下去,基本把各个区间的和都准备好了
你如果查L–R=1-2的累加和,咱们直接从头结点开始查,下面二分去找,1–2的累加和就是2,速度很快,o(log(n)速度

这个事情的本质就是:用额外空间,帮助加速查询累加和,达到优化时间复杂度的目的。
这个事情的本质就是:用额外空间,帮助加速查询累加和,达到优化时间复杂度的目的。
这个事情的本质就是:用额外空间,帮助加速查询累加和,达到优化时间复杂度的目的。

众所周知,但凡想要算法快,就要耗费额外空间,但凡想节约空间,就需要拖慢算法的速度。

这个做法类似于堆:
当年咱们把数组heap,逻辑上想象为一个完全二叉树,如何访问呢?就是通过下标的关系
下文讲得很清楚:
【1】堆,系统堆结构,手动改写堆结构,堆结构远比比堆排序更重要
从heap数组的i=0位置,开始,往下推
左子left=2i+1
右子right=2i+2
在这里插入图片描述
今天,咱们为了不那么复杂,咱们arr的下标i,不要0,从1开始记,看下图
因此左子和右子
分别为2i和2i+1
在这里插入图片描述
是不很简单!

当然,咱们今天要讲得线段树,它和堆不一样,堆可以是完全二叉树,但是今天的线段树是满二叉树,
因为我们要利用二分法,把arr的1–N范围,2分为一段一段的区间,每一段,代表arr的l–r范围【小写l和r哦】,
我们待会要用额外的数组p来记录区间l–r上的信息,这个信息p,就是咱们要干的3个任务的标记信息【先不说啥信息】
这里信息是p数组,p[i]就是整体l–r范围上的信息,下面我们会解释p信息具体是啥。

头结点下标为i=1,它代表arr的l–r范围,其实是固定的是1–N范围,N是arr的长度
往下分,mid=(l+r)/2
下标2节点代表l–mid范围,下标3节点代表mid+1–r范围
下标4节点又是二分2节点的范围,下面蓝色l–mid,mid+1–r
下标5节点又是二分3节点的范围,下面粉色L–mid,mid+1–r
……
在这里插入图片描述
直到所有叶节点都是一个节点,就代表一个整体范围
比如下图右边那个二叉树,N=15个节点,i=1开始,它代表arr的l-r=1–15范围内的信息,然后不断地推二叉树的下标们
每个左子和右子的下标,都代表来自父节点l–r范围内整体的一半信息,l–mid,mid+1–r

这里信息是p数组,p[i]就是整体l–r范围上的信息,下面我们会解释p信息具体是啥。
在这里插入图片描述
okay,你知道了吧,i从1开始,往下递推下标,其实代表的是arr的特定的l–r范围的信息
我们利用额外p数组,p[i]就是整体l–r范围上的信息

现在,给你一个数组arr,请你在给定的L–R这段区间范围上的完成这三种任务:

【1】add(L,R,C),在arr的L–R区间上整体每一个元素都 C,
【2】update(L,R,C),把arr的L–R区间上整体每一个元素都更新为C,
【3】query(L,R),请返回arr的L–R区间上整体每一个元素累加和

以上三个任务,每次调用任务时,咱们都把任务发到头结点,i=1位置,它代表的是l–r=1–N范围,因此让上面仨函数,统统带上i,l,r三个固定的参数,这仨参数是固定不变的,
当然了,递推下标i时,它代表的lr是变化的,这仨参数又是可变的。

L,R,C是不同任务范围和操作的C,仨都是可变的参数。

我们把线段树定义为SegmentTree类,它有arr,有信息p数组,有三大任务函数,下面挨个说


给定arr,准备信息p数组

我们说了,不用0下标,所以arr需要扩到N+1长度,把0–N-1转移到1–N上来

信息p可能是下面几种数组:
(1)sum数组:对应下标i处放信息:sum[i],它代表此时arr的l–r范围内的累加和【每次任务都可能让sum变化】
(2)lazy数组:对应下标i处放信息:lazy[i],它代表此时arr的l–r范围内下了一个增加任务,增加的值是多少呢?C,【揽下发音类似于lazy,所以取名为lazy,方便记忆】
(3)change数组:对应下标i处放信息:change[i],它代表此时arr的l–r范围内揽下了更新任务,更新后arr的l–r统统为多少?全是C
(4)isUpdated数组:配合change数组用,当有更新任务时,isUpdated[i]=true,代表l–r范围内就有更新任务,更新的具体任务看change数组,默认情况isUpdated[i]=false,表示当前l–r范围内没有揽下更新任务

以上所有p数组,他们的长度是多少呢?4N
为啥呢?
我们说过,这个二叉树是一个满二叉树,如果arr长度为N,则二分下去,就需要N个叶节点,非叶节点N-1个,总共就是2N
那为啥要4N?

情况是这样的,万一,长度是N没法组合出满二叉树,恰巧就在刚刚满的二叉树得叶节点右边多一个点
比如,N=8好说,1–8恰好能搞出一个满二叉树来
但是N=9呢,这9的话,就得额外把二分范围扩充到1–16了,这样仍然是可以满二叉树,也就是arr的N必须是2的x次方才行
看下图右边那种arr,9长度的,你就得额外多准备很多空间
在这里插入图片描述
故叶节点至少2N个节点,非业节点,2N-1个,总体4N-1个,那么p数组信息长为4N即可

好,下面我们来准备p信息数组们

    //然后再,面试中,要考虑用线段树,将复杂度降到o(logN)这样子提升身段
    public static class SegmentTree{
        public int[] arr;//转移新数组,不要坐标0
        public int[] sum;//起初就要建树,线段树的真个树的累加和,二分下去,左右收集
        public int[] lazy;//增加任务的,任务记录信息
        public int[] change;//update任务的记录信息
        public boolean[] isUpdated;//配合change任务的Boolean数组

        //首先拿到数组,先转移到N+1长度的数组中,然后开始build sum信息,目前这个数组arr就可以整好当前所有sum

        public SegmentTree(int[] origin){
            //传一个origin数组进来
            int N = origin.length + 1;//不用0下标,简单
            arr = new int[N];
            for (int i = 0; i < origin.length; i++) {
                arr[i + 1] = origin[i];//转移好
            }

            sum = new int[4 * N];
            lazy = new int[4 * N];
            change = new int[4 * N];
            isUpdated = new boolean[4 * N];
        }


给定arr,创建累加和sum信息

有了上面的准备,最开始来了一个arr
咱们就要把最初的sum累加和信息构建好
说白了就是让你构建每个二分区间的累加和,耗费了这么多额外空间,但是一旦问你L–R上累加和是多少?你直接o(log(n))速度二分下去查累加和,返回来给出答案。这样的话,咱就不要遍历L–R上的元素相加了【o(n)速度慢】

所以,arr一来,咱就递归去填写sum数组
递归过程中,遇到i位置怎么填?很简单,左子2i和右子2i+1的sum信息,综合加起来,即使i的sum信息
下图粉色下标那,你看看,2位置和是2,3位置和是2,故1位置作为父节点,就是左子2和右子3的和,2+2=4
在这里插入图片描述
代码这么玩:

        //然后定义汇总sum的build函数,把左子右子收集出来
        public void sumUp(int i){
            //当前rt左右子的信息,我当前rt就代表l--r
            sum[i] = sum[i << 1] + sum[i << 1 | 1];//2i和2i+1的信息汇总出来,放在我rt
        }

这里的下标,你想快速运算,就用位运算
在这里插入图片描述
任意下标汇总sum有了

那让你在l–r范围内汇总,你会了吧
先去左边递归收集左边部分的信息,然后去右边收集右边部分的信息
最后汇总sum给i
递归终止条件,l=r时,就一个元素,返回它就是了比如,下图5 6 7 8这种节点,就一个元素,返回arr[l]=arr[r]都行的
在这里插入图片描述
代码很容易:

        //一开始构建sum数组
        public void build(int l, int r, int i){
            //从l--r范围上填写信息,rt代表l--r,实际调用直接就1--N,i==1位置开始
            if (l == r) {
                sum[i] = arr[l];//直接填叶节点的值
                return;
            }

            //否则先去左边一半填写,再去右边一半填写,最后汇总
            int mid = (l + r) >> 1;//一半
            build(l, mid, i << 1);//左子
            build(mid + 1, r, i << 1 | 1);//右子,建好了以后
            //汇总
            sumUp(i);//左右子相加返回sun[i]中放着即可
        }

这种递归咱们玩实在太多了。
mid下标的计算,你想快速求则用位运算:
在这里插入图片描述
是不是很容易?


add(L,R,C,l,r,i)任务

来了新任务L–R上整体每个元素加C,
咱们把这个任务散播到头结点,也就是整个1–N范围上,让二分法往下找,找到L–R段上,让他们加C
具体来到l–r上时,

(1)如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,不要再往下发了,咱直接在l–r上做加C的任务【然后汇总信息往父节点报,调用 sumUp(i);】
(0)每次干任务之前,检查一下之前有没有来过任务,之前有任务的话,我接不住,要把这个旧任务给左右子干,然后我才能接当前这个新任务,调用sendTaskDown(int i, int ln, int rn),i的左子,右子,分别干ln和rn个,替我分忧。
(2)mid=(l+r)/2,如果L<=mid,说明L–R的任务还需要让左子树知道,故需要下发给左子树l–mid范围,即调用add(L,R,C,l,mid,i)
(3)mid=(l+r)/2,如果R>mid,说明L–R的任务还需要让右子树知道,故需要下发给右子树mid+1–r范围,即调用add(L,R,C,mid+1,r,i)

比如arr=1 1 1 1
一开始咱就有了sum= 4 2 2 1 1 1 1
在这里插入图片描述
如果你来了新任务add(L=1,R=2,C=1,l=1,r=4,i=1)
调用任务时l=1,r=4,i=1固定的不变
LRC是可变的

okay,咱们需要在L–R=1–2上每个都加C=1,看下图
(1)如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,不要再往下发了,咱直接在l–r上做加C的任务
这里r=4,显然不满足(1)条件,需要下发,发给谁呢?
(0)每次下发任务之前,检查一下之前有没有来过任务,之前有任务的话,我接不住,要把这个旧任务给左右子干,然后我才能接当前这个新任务,调用sendTaskDown(int i, int ln, int rn),i的左子,右子,分别干ln和rn个,替我分忧。【咱们现在不讲,你就知道这个意思就行,目前我i是空手,可以接新任务】
(2)mid=(l+r)/2,如果L<=mid,说明L–R的任务还需要让左子树知道,故需要下发给左子树l–mid范围,即调用add(L,R,C,l,mid,i)
这里,mid=(1+4)/2=2,L=1<mid=2,显然左边需要知道这个任务,下发给左边,调用add(L=1,R=2,C=1,l=1,r=mid,i=1)
(3)mid=(l+r)/2,如果R>mid,说明L–R的任务还需要让右子树知道,故需要下发给右子树mid+1–r范围,即调用add(L,R,C,mid+1,r,i)
这里,mid=2,因为R=2,R不>mid=2哦,所以右子不需要知道这个任务,它们不干这事。
在这里插入图片描述
上面调用add(L=1,R=2,C=1,l=1,r=mid,i=1)调用之后
l=1,r=2,L=1,R=2
恰好满足(1):如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,不要再往下发了,咱直接在l–r上做加C的任务
此时让sum[i=2] += (r-l+1)×C = 2×1,这就是咱们要给l–r段增加的累加和。
同时,咱们要让lazy[i=2] += C,代表l–r段,增加了C任务——lazy就是拿来记录增加任务这件事的,增加了谁?C
干完这个任务,注意要逐层返回给上面的节点们,也就是汇总信息,告诉父节点,我这范围改改变,你们也跟着变!
调用 sumUp(i);

懂了吧,反正来了L–R加C的任务,
从头结点,开始下发,然后咱们去找,哪些l–r段确实需要接下这个任务,下发任务之前,检查之前我还有旧任务吗?有旧任务话,我需要左右子分担,然后我才能揽住新任务,揽住了任务之后,干活,干完标记lazy,标记完汇总信息告诉父节点们,我变了!你们也变。

okay,撸代码这样的:

        //然后可以定义add任务
        //从当前l--r,i==1位置操作,往L--R上增加C;lazy新任务来了
        public void add(int L, int R, int C, int l, int r, int i){
            //若果任务范围包了当前lr范围,则直接加
            if (L <= l && r <= R){
                lazy[i] += C;//递增,因为可能之前也有增加任务
                sum[i] += (r - l + 1) * C;//累加这么多
                return;
            }
            //没有包,先把当前范围的任务下发,有的话
            //再去左右发当前的任务,最后汇总
            int mid = (l + r) >> 1;
            //先检查下发任务
            sendTaskDown(i, mid - l + 1, r - mid);//先给左右子发之前攒下的任务
            if (L <= mid) add(L, R, C, l, mid, i << 1);//如果任务范围还在mid左边,则左边需要接你的任务
            if (R > mid) add(L, R, C, mid + 1, R, i <<1 | 1);//任务范围R在mid右边,则右子需要接任务
            //汇总
            sumUp(i);
        }

搞定之后,情况是这样的,1 2 位置各加1,1–2范围内多了2,1–4整体也是多了2,下面绿色那些变化的数
在这里插入图片描述


update(L,R,C,l,r,i)任务

更新任务,和add如出一辙!
来了新任务L–R上整体每个元素,直接更新为C,就是变C,
咱们把这个任务散播到头结点,也就是整个1–N范围上,让二分法往下找,找到L–R段上,让他们整体变C

具体来到l–r上时,

从头结点,开始下发,然后咱们去找,哪些l–r段确实需要接下这个任务,下发任务之前,检查之前我还有旧任务吗?有旧任务话,我需要左右子分担,然后我才能揽住新任务,揽住了任务之后,干活(这个活,是更新任务哦),干完标记lazy,change和isChanged,标记完汇总信息告诉父节点们,我变了!你们也变。

(1)如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,不要再往下发了,咱直接在l–r上做 C的任务【然后汇总信息往父节点报,调用 sumUp(i);】
(0)每次干任务之前,检查一下之前有没有来过任务,之前有任务的话,我接不住,要把这个旧任务给左右子干,然后我才能接当前这个新任务,调用sendTaskDown(int i, int ln, int rn),i的左子,右子,分别干ln和rn个,替我分忧。
(2)mid=(l+r)/2,如果L<=mid,说明L–R的任务还需要让左子树知道,故需要下发给左子树l–mid范围,即调用update(L,R,C,l,mid,i)
(3)mid=(l+r)/2,如果R>mid,说明L–R的任务还需要让右子树知道,故需要下发给右子树mid+1–r范围,即调用update(L,R,C,mid+1,r,i)

比如arr=1 1 1 1
一开始咱就有了sum= 4 2 2 1 1 1 1
在这里插入图片描述
如果你来了新任务update(L=1,R=2, C=2 ,l=1,r=4,i=1)
调用任务时l=1,r=4,i=1固定的不变
LRC是可变的

okay,咱们需要在L–R=1–2上每个都变C=2,看下图
(1)如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,不要再往下发了,咱直接在l–r上做变C的任务
这里r=4,显然不满足(1)条件,需要下发,发给谁呢?
(0)每次下发任务之前,检查一下之前有没有来过任务,之前有任务的话,我接不住,要把这个旧任务给左右子干,然后我才能接当前这个新任务,调用sendTaskDown(int i, int ln, int rn),i的左子,右子,分别干ln和rn个,替我分忧。【咱们现在不讲,你就知道这个意思就行,目前我i是空手,可以接新任务】
(2)mid=(l+r)/2,如果L<=mid,说明L–R的任务还需要让左子树知道,故需要下发给左子树l–mid范围,即调用update(L,R,C,l,mid,i)
这里,mid=(1+4)/2=2,L=1<mid=2,显然左边需要知道这个任务,下发给左边,调用update(L=1,R=2,C=2,l=1,r=mid,i=1)
(3)mid=(l+r)/2,如果R>mid,说明L–R的任务还需要让右子树知道,故需要下发给右子树mid+1–r范围,即调用update(L,R,C,mid+1,r,i)
这里,mid=2,因为R=2,R不>mid=2哦,所以右子不需要知道这个任务,它们不干这事。
在这里插入图片描述
上面调用update(L=1,R=2,C=2,l=1,r=mid,i=1)调用之后
l=1,r=2,L=1,R=2
恰好满足(1):如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,不要再往下发了,咱直接在l–r上做变C的任务
此时让sum[i=2] = (r-l+1)×C = 2×1,这不是sum+=哦!!!而是sum=,更新操作,直接变,变成了新的l–r段的累加和。
同时,咱们要让lazy[i=2] = 0,代表l–r段,之前增加了的任务全部废了,因为我是update操作
同时,咱们要让change[i] = C,代表l–r段,来了一个新的更新为c的操作——这个C跟lazy类似,记录更新操作
通知,咱们要让isUpdated[i] = true,代表l–r段,确实有更新操作——配合change用

干完这个任务,注意要逐层返回给上面的节点们,也就是汇总信息,告诉父节点,我这范围改改变,你们也跟着变!
调用 sumUp(i);

代码手撕:

        //然后定义update任务
        public void update(int L, int R, int C, int l, int r, int i){
            if (L <= l && r <= R){
                isUpdated[i] = true;
                change[i] = C;
                sum[i] = (r - l + 1) * C;//更新任务只看自己,不是递增的关系
                lazy[i] = 0;//本节点的增加任务被废了
                return;
            }
            //没有包范围的话,需要先将攒的任务下发,再发本次任务
            int mid = (r + l) >> 1;
            //先检查下发任务
            sendTaskDown(i, mid - l + 1, r - mid);//分别往左右发这么多
            if (L <= mid) update(L, R, C, l, mid, i << 1);
            if (R > mid) update(L, R, C, mid + 1, r,i << 1 | 1);
            //左右需要的话,下方本任务,然后汇总
            sumUp(i);
        }

干完活,sum变了
在这里插入图片描述


检查l–r是否曾经接过旧任务,有就需要左右子分担

上面的add和update操作,在来了新任务之后,
都干了一件事:

从头结点,开始下发,然后咱们去找,哪些l–r段确实需要接下这个任务,下发任务之前,检查之前我还有旧任务吗?有旧任务话,我需要左右子分担,然后我才能揽住新任务,揽住了任务之后,干活(这个活,是更新任务哦),干完标记lazy或者change和isChanged,标记完汇总信息告诉父节点们,我变了!你们也变。

这里面的这个条件:
(0)每次下发任务之前,检查一下之前有没有来过任务,之前有任务的话,我接不住,要把这个旧任务给左右子干,然后我才能接当前这个新任务,调用sendTaskDown(int i, int ln, int rn),i的左子,右子,分别干ln和rn个,替我分忧。【咱们之前不讲,现在来讲】

目的是想让我i腾空手,只有目前我i是空手,才可以接新任务

检查怎么检查?无非就是检查是否有更新任务?是否有增加任务?
而且是先检查更新任务,因为更新任务会废掉增加任务,前面增加任务再多都没卵用,一段来了更新任务,直接以更新任务为核心任务

add和update调用时,调用sendTaskDown(i, mid - l + 1, r - mid);//分别往左右发这么多
左边mid-l+1个,左子承担这么多
右子承担r-mid个

检查下发函数如下:
首先检查update任务:
左右子的sum,lazy,change,和isChanged数组全部检查变为我i的任务。
分发下去后,我isChanged=false,代表我i是空手的。
其他不管,因为add和update来了直接变sum,lazy,change,会覆盖的。

再检查add任务:
只需要把左右子的sum和lazy变我i的信息就行
分发下去之后,我lazy[i] = 0,代表我i从此是空手了。

        //然后定义提前检查下发任务的函数,把更新任务和增加任务先下发给左右子
        //pushDown方法,先查更新任务,因为更新任务优先级高,再查增加任务
        public void sendTaskDown(int i, int ln, int rn){
            //在rt上看,有之前攒下来的任务,需要下发的,左子有多少个呢,ln,右子有多少个节点呢,ln
            //左子右子都要承担这些任务,信息更新到sum,lazy和change与isUpdated中
            //操作的范围是l--r,当前代表坐标是rt
            //左子承担多少个mid-l + 1个,不管是加,还是更新
            //右子承担多少个r-mid 个,不管是加还是更新
            if (isUpdated[i]){
                //如果本节点之前有任务揽住了,需要先下发给下面的范围,左子右子都需要干的任务,再接新的
                //是更新的,左子右子都得更新,啥增加,啥更新任务都清掉
                //更新是颠覆性的,所有sum改变,lazy清零
                sum[i << 1] = ln * change[i];//我rt有一个更新过的任务,下面所有人都要叠加这么多个
                sum[i << 1 | 1] = rn * change[i];//左右子都是
                lazy[i << 1] = 0;//清零,方便接新的任务
                lazy[i << 1 | 1] = 0;//清零,接新任务
                change[i << 1] = change[i];
                change[i << 1 | 1] = change[i];//承担我的
                isUpdated[i << 1] = true;//标记已经承担过更新任务
                isUpdated[i << 1 | 1] = true;//标记左右子都承担过update任务
                isUpdated[i] = false;//分发完以后,跟我无关了,意思是我清了
            }

            //再看看有没有之前揽过的增加任务,也需要干
            if (lazy[i] != 0){
                //不为0就说明我有增加任务,需要下发,注意,下发是+=,
                // 因为下面的那些可能还有多个任务,所以是递增的
                lazy[i << 1] += lazy[i];
                lazy[i << 1 | 1] += lazy[i];
                sum[i << 1] += ln * lazy[i];//左子整体增加
                sum[i << 1 | 1] += rn * lazy[i];//右子也整体增加
                lazy[i] = 0;//清空我任务
            }
        }//所有任务干活之前,都需要把之前揽住的任务下发,再去看别的,再汇总,所以这个是通用的代码


query(L,R,l,r,i)任务

query就更简单了,不干活,直接查,就是二分法查而已
但是查之前,往下分发任务,轮到谁干,你干完,把信息给我,返回sum

来了新任务L–R上整体每个元素,查sum,返回,
咱们把这个任务散播到头结点,也就是整个1–N范围上,让二分法往下找,找到L–R段上,查到sum返回

具体来到l–r上时,

从头结点,开始下发,然后咱们去找,哪些l–r段确实需要接下这个任务,下发任务之前,检查之前我还有旧任务吗?有旧任务话,我需要左右子分担,然后我才能揽住新任务,揽住了任务之后,干活(这个活,是返回累加和的任务哦)直接返回就行。

(1)如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,不要再往下发了,直接返回sum[i]
(0)每次干任务之前,检查一下之前有没有来过任务,之前有任务的话,我接不住,要把这个旧任务给左右子干,然后我才能接当前这个新任务,调用sendTaskDown(int i, int ln, int rn),i的左子,右子,分别干ln和rn个,替我分忧。
准备ans=0,去收集左右的信息,融合加起来给ans,最后作为返回结果
(2)mid=(l+r)/2,如果L<=mid,说明L–R的任务还需要让左子树知道,故需要下发给左子树l–mid范围,即调用query(L,R,l,mid,i)
(3)mid=(l+r)/2,如果R>mid,说明L–R的任务还需要让右子树知道,故需要下发给右子树mid+1–r范围,即调用query(L,R,mid+1,r,i)
(4)ans=(2)+(3),返回ans;

比如arr=1 1 1 1
一开始咱就有了sum= 4 2 2 1 1 1 1
在这里插入图片描述
如果你来了新任务query(L=1,R=2,l=1,r=4,i=1)
调用任务时l=1,r=4,i=1固定的不变
LRC是可变的

okay,咱们需要在L–R=1–2上每个都变C=2,看下图
(1)如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,不要再往下发了,咱直接在l–r上做变C的任务
这里r=4,显然不满足(1)条件,需要下发,发给谁呢?
(0)每次下发任务之前,检查一下之前有没有来过任务,之前有任务的话,我接不住,要把这个旧任务给左右子干,然后我才能接当前这个新任务,调用sendTaskDown(int i, int ln, int rn),i的左子,右子,分别干ln和rn个,替我分忧。【咱们现在不讲,你就知道这个意思就行,目前我i是空手,可以接新任务】
准备ans=0,去收集左右的信息,融合加起来给ans,最后作为返回结果
(2)mid=(l+r)/2,如果L<=mid,说明L–R的任务还需要让左子树知道,故需要下发给左子树l–mid范围,即调用query(L,R,l,mid,i)
这里,mid=(1+4)/2=2,L=1<mid=2,显然左边需要知道这个任务,下发给左边,调用query(L=1,R=2,l=1,r=mid,i=1)
(3)mid=(l+r)/2,如果R>mid,说明L–R的任务还需要让右子树知道,故需要下发给右子树mid+1–r范围,即调用query(L,R,mid+1,r,i)
这里,mid=2,因为R=2,R不>mid=2哦,所以右子不需要知道这个任务,它们不干这事。
在这里插入图片描述
上面调用query(L=1,R=2,l=1,r=mid,i=1)调用之后
l=1,r=2,L=1,R=2
恰好满足(1):如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,不要再往下发了,咱直接返回sum[i=2]=2

最终ans+=2,返回ans
在这里插入图片描述
整体查到1–2上的累加和就是ans=2;

撸一下代码:

        //然后定义query==查询一段累加和任务
        public long query(int L, int R, int l, int r, int i){
            if (L <= l && r <= R){
                //任务刚好包了就直接算
                return sum[i];
            }
            //每包就需要左右收集,先下发攒过的任务
            int mid = (r + l) >> 1;
            //先检查下发任务
            sendTaskDown(i, mid - l + 1, r - mid);//左右分发多少个
            long ans = 0;
            if (L <= mid) ans += query(L, R, l, mid, i << 1);
            if (R >mid) ans += query(L, R, mid + 1, r, i << 1 | 1);
            return ans;
        }

到此,query就轻而易举搞定了
速度非常快,每次往下查二叉树,二分法就是o(log(n))的速度!比暴力遍历快。

线段树的操作思路,三个任务都一样

(1)从头结点,开始下发任务,然后咱们去找,哪些l–r段确实需要接下这个任务,
(2)下发任务之前,检查之前我还有旧任务吗?有旧任务话,我需要左右子分担,然后我才能揽住新任务,
(3)揽住了任务之后,干活(这个活,可能是增加,可能是更新,也可能是返回累加和的任务哦),
(4)是add和update任务,则干完标记lazy,change和isChanged,标记完汇总信息告诉父节点们,我变了!你们也变。
(5)是query任务需要去左右树收集累加和信息,然后加到ans上,直接返回ans即可

这就是线段树:区间数,是很复杂,你要捋清咱们的目的,是用额外空间,帮助加速查询累加和,达到优化时间复杂度的目的。

手撕代码,遇到题目的时候,可以想想是否和区间操作有关,有的话,可以用这个方法
虽然很难!

手撕线段树的代码:

    //这个事情的本质就是:用额外空间,帮助加速查询累加和,达到优化时间复杂度的目的。
    public static class SegmentTree{
        //成员属性
        //(1)sum数组:对应下标i处放信息:sum[i],它代表此时arr的l–r范围内的累加和【每次任务都可能让sum变化】
        public int[] sum;
        //(2)lazy数组:对应下标i处放信息:lazy[i],它代表此时arr的l–r范围内揽下了一个增加任务,
        // 增加的值是多少呢?C,【揽下发音类似于lazy,所以取名为lazy,方便记忆】
        public int[] lazy;
        //(3)change数组:对应下标i处放信息:change[i],它代表此时arr的l–r范围内揽下了更新任务,
        // 更新后arr的l–r统统为多少?全是C
        public int[] change;
        //(4)isUpdated数组:配合change数组用,当有更新任务时,isUpdated[i]=true,
        // 代表l–r范围内就有更新任务,更新的具体任务看change数组,默认情况isUpdated[i]=false,
        // 表示当前l–r范围内没有揽下更新任务
        public boolean[] isChanged;
        //转移arr0到arr
        public int[] arr;//不要0下标


        //构造函数
        //构造的时候,就告知arr是啥,我好抹掉0下标
        public SegmentTree(int[] arr0){
            //先抹掉下标0
            arr = new int[arr0.length + 1];
            int N = arr.length;
            for (int i = 0; i < arr0.length; i++) {
                arr[i + 1] = arr0[i];
            }

            //现在开始都是操作arr,1--N
            //初始化成员属性,4N宽度
            sum = new int[N << 2];//x<<1=2x
            lazy = new int[N << 2];//x<<1=2x
            change = new int[N << 2];//x<<1=2x
            isChanged = new boolean[N << 2];//x<<1=2x
        }

        //首次给你arr,请你用递归构建满二叉树,把累加和sum构建出来
        //从arr的l--r上构建
        //先建左,再建右,最后综合建我的
        public void build(int l, int r, int i){
            //从i=1开始填写,i=0这个位置的信息就代表l--r范围上的累加和sum
            //base case,l==r,就是每一个元素触底
            if (l == r) {
                sum[i] = arr[l];//取lr均可
                return;
            }

            //左右构建,并求和上来
            int mid = l + ((r - l) >> 1);
            build(l, mid, i << 1);//i<<1=2i
            build(mid + 1, r, i << 1 | 1);//i<<1 | 1=2i+1

            //然后把左右子,汇总给我sum[i]--不管啥任务,都是围绕变动sum而进行的
            sumUp(i);//把i下标的左右子汇总给我i
        }

        //把i下标的左右子汇总给我i
        public void sumUp(int i){
            sum[i] = sum[i << 1] + sum[i << 1 | 1];
        }

        //成员方法
        //检查l–r是否曾经接过旧任务,有就需要左右子分担
        //从头结点,开始下发,然后咱们去找,哪些l–r段确实需要接下这个任务,下发任务之前,
        // 检查之前我还有旧任务吗?有旧任务话,我需要左右子分担,然后我才能揽住新任务,
        // 揽住了任务之后,干活(这个活,是更新任务哦),干完标记lazy或者change和isChanged,sum
        public void checkPreTaskDown(int i, int ln, int rn){
            //把i位置之前接的任务,分发下去给左子和右子分担,i上的任务时r-L+1个,那
            //左子承担mid - l + 1个
            //右子承担r-mid个
            //不过先查update任务,它可以干废add任务
            //更新任务涉及sum,change,isChanged,lazy
            if (isChanged[i]){//之前i的l--r范围内有任务,下发
                lazy[i << 1] = 0;//左右子的add任务,被update废了
                lazy[i << 1 | 1] = 0;//左右子的add任务,被update废了
                sum[i << 1] = ln * change[i];//change中放着i更新为谁了,你现在就可以分发
                sum[i << 1 | 1] = rn * change[i];//change中放着i更新为谁了,你现在就可以分发
                change[i << 1] = change[i];//左子右子的change完全承接i
                change[i << 1 | 1] = change[i];//左子右子的change完全承接i
                isChanged[i << 1] = true;//左子右子的isChanged完全承接i
                isChanged[i << 1 | 1] = true;//左子右子的isChanged完全承接i
                //然后更新任务重要的一步就是废掉add,同时自己的update也清空
                isChanged[i] = false;//我i的update任务没了
                lazy[i] = 0;
            }

            //后检查add任务
            //add任务涉及sum,lazy
            if (lazy[i] != 0){//确实接过任务,干
                sum[i << 1] += ln * lazy[i];//加过的C给左右子
                sum[i << 1 | 1] += ln * lazy[i];//加过的C给左右子
                lazy[i << 1] += lazy[i];//lazy记录纸i的l--r要加的C,给左右子
                lazy[i << 1 | 1] += lazy[i];//lazy记录纸i的l--r要加的C,给左右子
                lazy[i] = 0;//然后清掉我i的任务
            }
        }

        //add(L,R,C,l,r,i)任务
        public void add(int L, int R, int C, int l, int r, int i){
            //带着L--R上要加C的任务,来到i位置,i位置代表l--r范围内的信息,决定分下去还是不分?
            //(1)如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,
            // 不要再往下发了,咱直接在l–r上做加C的任务【然后汇总信息往父节点报,调用 sumUp(i);】
            if (L <= l && r <= R){
                //干
                sum[i] += (r - l + 1) * C;
                lazy[i] += C;//增加任务,叠加的关系
                return;//base case结束,返回,看看左右子还需要下发吗,不需要就汇总信息返回
            }
            //(0)每次干任务之前,检查一下之前有没有来过任务,之前有任务的话,我接不住,
            // 要把这个旧任务给左右子干,然后我才能接当前这个新任务,
            // 调用sendTaskDown(int i, int ln, int rn),i的左子,右子,分别干ln和rn个,替我分忧。
            int mid = l + ((r - l) >> 1);
            checkPreTaskDown(i, mid - l + 1, r - mid);
            //(2)mid=(l+r)/2,如果L<mid,说明L–R的任务还需要让左子树知道,
            // 故需要下发给左子树l–mid范围,即调用add(L,R,C,l,mid,i)
            if (L <= mid) add(L, R, C, l, mid, i << 1);
            //(3)mid=(l+r)/2,如果R>mid,说明L–R的任务还需要让右子树知道,故需要下发给右子树mid+1–r范围,
            // 即调用add(L,R,C,mid+1,r,i)
            if (R > mid) add(L, R, C, mid + 1, r, i << 1 | 1);

            //下发完最新任务给左右子,汇总左右子信息返回给我sum
            sumUp(i);
        }

        //update(L,R,C,l,r,i)任务
        public void update(int L, int R, int C, int l, int r, int i) {
            //带着L--R上要变C的任务,来到i位置,i位置代表l--r范围内的信息,决定分下去还是不分?
            //(1)如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,不要再往下发了,
            // 咱直接在l–r上做 变 C的任务【然后汇总信息往父节点报,调用 sumUp(i);】
            if (L <= l && r <= R){
                //干,整体更新,覆盖增加任务
                sum[i] = (r - l + 1) * C;
                lazy[i] = 0;//更新任务直接会废掉增加任务的
                change[i] = C;//增加任务,叠加的关系
                isChanged[i] = true;//标记i的l--r范围内有更新任务
                return;//base case结束,返回,看看左右子还需要下发吗,不需要就汇总信息返回
            }
            //(0)每次干任务之前,检查一下之前有没有来过任务,之前有任务的话,我接不住,
            // 要把这个旧任务给左右子干,然后我才能接当前这个新任务,
            // 调用sendTaskDown(int i, int ln, int rn),i的左子,右子,分别干ln和rn个,替我分忧。
            int mid = l + ((r - l) >> 1);
            checkPreTaskDown(i, mid - l +1, r - mid);
            //(2)mid=(l+r)/2,如果L<mid,说明L–R的任务还需要让左子树知道,
            // 故需要下发给左子树l–mid范围,即调用update(L,R,C,l,mid,i)
            if (L <= mid) update(L, R, C, l, mid, i << 1);
            //(3)mid=(l+r)/2,如果R>mid,说明L–R的任务还需要让右子树知道,
            // 故需要下发给右子树mid+1–r范围,即调用update(L,R,C,mid+1,r,i)
            if (R > mid) update(L, R, C, mid + 1, r, i << 1 | 1);
            //下发完最新任务给左右子,汇总左右子信息返回给我sum
            sumUp(i);
        }

        //query(L,R,l,r,i)任务
        public int query(int L, int R, int l, int r, int i) {
            //带着L--R上要查找L--R上累加和的任务,来到i位置,i位置代表l--r范围内的信息,决定分下去还是不分?
            //(1)如果L–R恰好包住l–r,即L<=l && r<=R,说明l–r上直接可以揽住这个任务,不要再往下发了,
            // 直接返回sum[i]
            if (L <= l && r <= R) {
                //干,整体找到了这个范围,直接返回sum[i],代表覆盖l--r的arr的累加和
                return sum[i];
            }
            //(0)每次干任务之前,检查一下之前有没有来过任务,之前有任务的话,我接不住,
            // 要把这个旧任务给左右子干,然后我才能接当前这个新任务,
            // 调用sendTaskDown(int i, int ln, int rn),i的左子,右子,分别干ln和rn个,替我分忧。
            int mid = l + ((r - l) >> 1);
            checkPreTaskDown(i, mid - l +1, r - mid);
            //准备ans=0,去收集左右的信息,融合加起来给ans,最后作为返回结果
            int ans = 0;
            //(2)mid=(l+r)/2,如果L<mid,说明L–R的任务还需要让左子树知道,
            // 故需要下发给左子树l–mid范围,即调用query(L,R,l,mid,i)
            if (L <= mid) ans += query(L, R, l, mid, i << 1);
            //(3)mid=(l+r)/2,如果R>mid,说明L–R的任务还需要让右子树知道,
            // 故需要下发给右子树mid+1–r范围,即调用query(L,R,mid+1,r,i)
            if (R > mid) ans += query(L, R, mid + 1, r, i << 1 | 1);
            //(4)ans=(2)+(3),返回ans;
            return ans;
        }
    }

测试一下:

    public static void test(){
        int[] arr = {1,1,1,1,1};
        System.out.println("原始数组");
        for(Integer i : arr) System.out.print(i +" ");
        System.out.println();

        SegmentTree segmentTree = new SegmentTree(arr);
        System.out.println("不要下标0的数组");
        for(Integer i : segmentTree.arr) System.out.print(i +" ");
        System.out.println();

        int[] sum = segmentTree.sum;
        int N = segmentTree.arr.length;
        System.out.println("arr的sum数组");
        segmentTree.build(1, N - 1, 1);//从i=1开始构建arr的1--N范围
        for(Integer i : sum) System.out.print(i +" ");
        System.out.println();

        //在1--2上整体加1看看,arr的l--r=1,代表线段树的下标时i=1,固定不变的
        segmentTree.add(1, 2, 1, 1, N - 1, 1);
        sum = segmentTree.sum;
        for(Integer i : sum) System.out.print(i +" ");
        System.out.println();

        //在1--5上整体变5看看,arr的l--r=1,代表线段树的下标时i=1,固定不变的
        segmentTree.update(1, 5, 5, 1, N - 1, 1);
        sum = segmentTree.sum;
        for(Integer i : sum) System.out.print(i +" ");
        System.out.println();

        //查询1--5上arr的累加和,arr的l--r=1,代表线段树的下标时i=1,固定不变的
        int ans = segmentTree.query(1, 5, 1, N - 1, 1);
        System.out.println(ans);
    }

    public static void main(String[] args) {
        test();
    }

跟测暴力累加那个一样

原始数组
1 1 1 1 1 
不要下标0的数组
0 1 1 1 1 1 
arr的sum数组
0 5 3 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 7 5 2 4 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 25 5 2 4 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
25

不容易的,这个线段树,代码是真的很复杂,你需要抠清楚细节,自己手撕代码
我是学了2遍,复习了不下3遍,今天自己捋清这个线段树,然后我自己亲自又手撕了一遍代码
这个代码不是看着谁抄的,就是我闭眼手写出来。

为什么要逼自己手写代码???
很简单,因为字节跳动的招聘笔试题目,它就要你闭眼写代码!你没办法用idea,你也不可能照着别人的代码抄,统统自己手写
你今天不辛苦逼自己手撕代码,到时候,上了考场,你不会,怎么进互联网大厂?

不知道面试会不会考这么难,万一考,你起码把思想说清楚,让面试官知道你会这个数据结构与算法
面试时大概率不会让你手撕线段树的所有代码,但是你要熟悉每个函数的功能,知道核心思想,至少你要会手撕一个add,update或者query的其中一个函数


总结

提示:重要经验:

1)线段树区间树,就是利用额外空间加速时间,通过二分法的形式,以最快的速度下发任务,查询任务,完成区间操作
2)面试时大概率不会让你手撕线段树的所有代码,但是你要熟悉每个函数的功能,知道核心思想,至少你要会手撕一个add,update或者query的其中一个函数
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
东南亚位于我国倡导推进的“一带一路”海陆交汇地带,作为当今全球发展最为迅速的地区之一,近年来区域内生产总值实现了显著且稳定的增长。根据东盟主要经济体公布的最新数据,印度尼西亚2023年国内生产总值(GDP)增长5.05%;越南2023年经济增长5.05%;马来西亚2023年经济增速为3.7%;泰国2023年经济增长1.9%;新加坡2023年经济增长1.1%;柬埔寨2023年经济增速预计为5.6%。 东盟国家在“一带一路”沿线国家中的总体GDP经济规模、贸易总额与国外直接投资均为最大,因此有着举足轻重的地位和作用。当前,东盟与中国已互相成为双方最大的交易伙伴。中国-东盟贸易总额已从2013年的443亿元增长至 2023年合计超逾6.4万亿元,占中国外贸总值的15.4%。在过去20余年中,东盟国家不断在全球多变的格局里面临挑战并寻求机遇。2023东盟国家主要经济体受到国内消费、国外投资、货币政策、旅游业复苏、和大宗商品出口价企稳等方面的提振,经济显现出稳步增长态势和强韧性的潜能。 本调研报告旨在深度挖掘东南亚市场的增长潜力与发展机会,分析东南亚市场竞争态势、销售模式、客户偏好、整体市场营商环境,为国内企业出海开展业务提供客观参考意见。 本文核心内容: 市场空间:全球行业市场空间、东南亚市场发展空间。 竞争态势:全球份额,东南亚市场企业份额。 销售模式:东南亚市场销售模式、本地代理商 客户情况:东南亚本地客户及偏好分析 营商环境:东南亚营商环境分析 本文纳入的企业包括国外及印尼本土企业,以及相关上下游企业等,部分名单 QYResearch是全球知名的大型咨询公司,行业涵盖各高科技行业产业链细分市场,横跨如半导体产业链(半导体设备及零部件、半导体材料、集成电路、制造、封测、分立器件、传感器、光电器件)、光伏产业链(设备、硅料/硅片、电池片、组件、辅料支架、逆变器、电站终端)、新能源汽车产业链(动力电池及材料、电驱电控、汽车半导体/电子、整车、充电桩)、通信产业链(通信系统设备、终端设备、电子元器件、射频前端、光模块、4G/5G/6G、宽带、IoT、数字经济、AI)、先进材料产业链(金属材料、高分子材料、陶瓷材料、纳米材料等)、机械制造产业链(数控机床、工程机械、电气机械、3C自动化、工业机器人、激光、工控、无人机)、食品药品、医疗器械、农业等。邮箱:market@qyresearch.com

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值