线段树学习笔记

线段树Segmenttree

参考于

线段树是常用的用来维护 区间信息 的数据结构。

对于树中的每一个节点都代表了整个序列中的一段子区间;对于树中的每个叶子节点都代表了序列中的单个元素;父节点存储的是其每一个子节点的信息。

线段树可以在Ο(log N)的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。

通过递归建造树,将一个长度不为1的区间平均划分,继续进行求解。

具体的:

设给定的区间范围为:[L , R] 且有 R - L >= 1 (即该区间长度不为1) ,则继续进行求解递归,划分两个新的区间为:

[L , R+L2 ] 与 [R+L2 +1 , R ] 直到 区间长度为1为止

为了方便说明,这里先给出例子:以数组 nums[1,6]=20,21,22,23,24,25为例进行建树

示例图

image-20220101113003155

其中 第一个元素表示 为线段树的第几号节点;第二个元素表示 为该节点管辖范围内的元素总和;第三个元素表示 为该节点的管辖范围。

​ 这里讲解一下,关于第二号节点(左孩子)是怎么来的。首先是第一个元素:其数组的节点号,由于二号节点是一号节点的左孩子,因此其节点号编为父节点编号×2= 2(右孩子为:父节点编号×2+1);然后是第二个元素:该节点管辖范围内的元素总和,根据分治算法的思想,如果当前管辖范围为1,即只有一个数字(rootLeft==rootRight),则令其等于nums数组中rootLeft下标所对应的数值。但是当前长度不为一,所以他所需要做的是将其左右孩子(4,5)管辖范围的总和累加,至于他两个孩子管辖总和和怎么求,那还是得先知道他们的孩子....(递归);再然后是其第三个元素:二号节点管辖的元素范围,根据上面的划分规则,可知,rootLeft=rootLeft=1rootRight=rootLeft+rootRight2=3。类似的,对于右孩子三号节点而言:rootLeft = rootLeft+rootRight2=4,rootRight = rootRight =6。至此我们大概知道怎么建树了。

成员属性

因此,此时就可以直接给出线段树的成员属性了

int[] nums: 暂时存储目标数组,即创建线段树时所依据的参数数组

int[] d: 线段树的管辖范围数组*(这里需要注意的一点是,为了方便,这里的nums数组以及d数组的起始下标都是1)。如上图中的 d[4] 则表示管辖数组nums范围 [1,2] 范围内的和,即 41

int[] t:懒惰标记 后面会讲到,主要用于延迟修改。

short[] flage: 配懒惰标记下放时使用,其主要功能为,防止区间赋值为0时导致下推失效,以及判断懒惰标记的区间是做了何种操作。该数组有三种状态:0--默认没有对该节点进行修改;-1--对该节点区间进行置值操作;1--对该节点区间进行加值操作。

通过上图不难看出,一个 长度为 n 的给定nums数组,其建的线段树深度为:H = [log2n+1]

​ 又由于该树为一棵完全二叉树,因此可得其所有节点数为:2[H]+11 = 4n - 1,因此可设d数组与t数组初始大小为4n-1


树的初始化

/**
     * @param _nums 代表传进来需要划分的数组
*/
public Segmenttree(int[] _nums) {
    // 线段树叶子节点可大约计算为 4n
    int len = _nums.length * 4 - 1;
    d = new int[len];
    t = new int[len];
    flage = new short[len];
    this.nums= _nums;
}

过程图示说明:

image-20220101113044402

​ 如果长度为一即,left==right 则令当前节点管辖的总和为原数组中下标为left的元素(因为当前也只管理了一个元素),如果长度不为一,则按照上面说明的划分规则进行递归。

​ 例如上图中的左边,一开始管辖长度不为1,因此会不停的递归直到长度为1:8号叶子节点,长度为一,代表的是原数组中的下标为1中的元素(20),其兄弟节点也是长度为一的区间,其代表的是原数组中的下标为2的元素(21)。至此求得了4号节点的左右孩子的总和,然后该节点的总和就是两节点的和:41。类似的,可以依次自底向上求得所需的元素节点,便完成了整棵树的构建过程。

建树

  /**
     * 构建线段树
     * @param left 代表左区间
     * @param right 代表右区间
     * @param rooot 代表当前的根节点(编号)
  */
     public void build(int left, int right, int root) {
        // 如果当前区间范围只有一个元素,则直接将对应的nums元素数组中对应的数字放进根节点中(代表所管辖的做右区间之和)
        if (left == right) {
            d[root] = nums[left];
            return;
        }

        // 取 left 与 right 的中间下标
        int m = left + ((right - left) >> 1);
        // 递归左右区间进行建树:
        // 建造左区间 [left,m] 并且当前的根节点为 root*2
        build(left, m, root * 2);
        // 建造右区间 [m+1,right]并且当前的根节点为 root*2+1
        build(m + 1, right, root * 2 + 1);
        // 由于是递归建树的,所以到这步时,该 root 节点的左(root*2)孩子右孩子(root*2+1)节点都已经建造好了
        // 因此,这时只需要按照线段树的定义,该root节点管辖的是左右孩子的总和,即:d[root] = d[root*2]+d[root*2+1]
        d[root] = d[root * 2] + d[root * 2 + 1];
    }

线段树的区间查找

比如查找 [left,rigth](aleft+aleft+1+..+aright) 中的总和,区间最小/大值等

查找区间总和

​ 还是以上面的例子说明如何查找区间总和,如果是要找[1,6]区间的总和,可以很明显的看出,该范围是刚好被一号节点所管辖,所以可以直接访问一号节点即可。但是如果要求的是[2,3]范围的呢?在图中并没有刚好管辖这个范围的节点,但是我可以将[2,3]分解成[2,2]与[3,3]来求解。这便是查找的主要思想,将查询区间拆分成由线段树节点管辖的最大范围的并集,例如,查询区间为[3,6] 那么由图我们可以得知线段树管辖区间内可以将其最大限度(在可直接求得总和的前提下将区间拆分成尽可能少的数量,就是极大子区间)的拆分成两个区间,即[3,3][4,6] 而不是[3,3][4,4][5,5][6,6]

​ 具体的,我们可以效仿递归建树的过程,不停的递归d中的节点。如果当前遍历到的节点所管辖的范围刚好是我们所需求的的元素总和的子集,可直接返回其管理的总和。如,二号节点管辖区间为[1,3]不是我们需求的[2,3]的子集,继续递归,直到递归到[2,2],是[2,3]的子集,直接return其总和:21,同理另外一个[3,3]也是如此,当遍历到五号节点时直接返回其总和:22。因此可求得[2,3]的区间总和为:22 + 21 = 43

​ 假设我们所需求的目标区间和为[sLeft,sRight],当前遍历到的节点所管辖的区间范围为[rootLeft,rootRigth]

  • 如果,当前遍历到的区间为所需求的子集: sLeft <= rootLeft && sRight >= rootRigth

    直接return 该节点总和

  • 否则,判断目标区间的范围与当前遍历的区间的左右孩子的管辖范围的关系:

    • sLeft <= rootLeft+rootRigth2 (左孩子的左边界) :说明与左孩子可能存在交集。进行左区间遍历
    • sRigtht >= rootLeft+rootRigth2+1 (右孩子的左边界) :说明与右孩子可能存在交集。进行右区间遍历

    image-20211231163840097

image-20220101113121621

/**
     * 主要思想为,将不可直接求的范围之和分解为线段树中存储好了的管辖极大子集进行求和:如[3,5]->[3,3]+[4,5]
     * 获取 nums 规定区间内的数值总和其范围为:[sumLeft,sumRight]
     * @param sumLeft 规定取值范围的左边界(包含于)
     * @param sumRight 规定取值范围的右边界(包含于)
     * @param root 当前遍历到的根节点 root
     * @param rootLeft 当前遍历到的根节点 root  的管辖区间的左边界(包含于)
     * @param  rootRight 当前遍历到的根节点 root  的管辖区间的右边界(包含于
     * @param return 求区间[sumLeft, sumRight] 的和
*/
public int getsum(int sumLeft, int sumRight, int root, int rootLeft, int rootRight) {
    //毫无交集
    if (rootRight < sumLeft || rootLeft > sumRight)
        return 0;
    // 当前遍历到的root节点所管辖的区间为要求求和的区间的子集时,将该子集[rootLeft,rootRight]返回
    if (sumLeft <= rootLeft && sumRight >= rootRight)
        return d[root];
    int m = rootLeft + ((rootRight - rootLeft) >> 1), sum = 0;
    // 如果查询区间与目前遍历到的根节点所管辖的左区间可能有交集的话,进行左区间遍历
    if (sumLeft <= m)
        sum += getsum(sumLeft, sumRight, root * 2, rootLeft, m);
    // 如果查询区间与目前遍历到的根节点所管辖的右区间可能有交集的话,进行右区间遍历
    if (sumRight > m)
        sum += getsum(sumLeft, sumRight, root * 2 + 1, m + 1, rootRight);
    return sum;
}
查找区间最小/大值

在实现要求上,想用线段树求解最大/小值还得另外创建两个数组,分别用来记录与这两数组对应的节点所管辖范围中的最大值与最小值。

    //储存当前节点所管辖范围内的最大值
    int[] max;
    //储存当前节点所管辖范围内的最小
    int[] min;
   /**
     * @param _nums 代表传进来需要划分的数组
     */
    public Segmenttree(int[] _nums) {
        // 线段树叶子节点可大约计算为 4n
        int len = _nums.length * 4 - 1;
        d = new int[len];
        t = new int[len];
        flage = new short[len];
        max = new int[len];
        min = new int[len];
        this.nums= _nums;
    }

另外,由于这两数组是依附于线段树上的,且两个属性不一样(一个最大值一个最小值),所以需要单独的使用构建方法。

    /**构建线段树的最大值的相关信息
     * @param left root节点管辖的左边区间
     * @param right root节点管辖的右边区间
     * @param root 当前遍历到的节点
     * @return root节点所管理的最大值
     */
    public int buildMax(int left, int right, int root) {
        //如果是无效的节点,为了防止干扰建树,因此返回极小值
        if (left > right) {
            return Integer.MIN_VALUE;
        }
        if (left == right) {
            //需要确保获得的最值是最新的,因此是使用区间查询线段树中的值,而不是直接赋值为nums[left]的值
            max[root] = this.getsum(left, left, 1, 1, this.nums.length - 1);
            return max[root];
        }
        max[root] = Math.max(buildMax(left, left + ((right - left) >> 1), root * 2),
                buildMax(left + ((right - left) >> 1) + 1, right, root * 2 + 1));
        return max[root];
    }

    /**构建线段树的最小值的相关信息
     * @param left root节点管辖的左边区间
     * @param right root节点管辖的右边区间
     * @param root 当前遍历到的节点
     * @return root节点所管理的最小值
     */
    public int buildMin(int left, int right, int root) {
        //如果是无效的节点,为了防止干扰建树,因此返回极大值
        if (left > right) {
            return Integer.MAX_VALUE;
        }
        if (left == right) {
            min[root] = this.getsum(left, left, 1, 1, this.nums.length - 1);
            return min[root];
        }
        min[root] = Math.min(buildMin(left, left + ((right - left) >> 1), root * 2),
                buildMin(left + ((right - left) >> 1) + 1, right, root * 2 + 1));
        return min[root];
    }

关于实现求区间最值思想与上面的求区间之和类似,只不过是将累加左右交集区间和的步骤换成了比较左右交集区间的最小值而已,然后将这两者中的最小值最为当前节点管辖的最小值返回。

查询[left,rigtht]中最小值

    /**
     * 查询区间[left,right]中的最小值
     */
    public int queryMin(int left, int right) {
        //查询区间不合法
        if (left > right)
            return -1;
        this.buildMin(1, this.nums.length - 1, 1);
        return this.findMin(left, right, 1, 1, this.nums.length - 1);
    }
   /**
     * @param left 所需查询区间的左边界
     * @param right 所需查询区间的有边界
     * @param root 当前遍历到的区间
     * @param rootLeft root管辖的区间左边界
     * @param rootRight root管辖的区间右边界
     * @return [left,right] 中的最小值
     */
    public int findMin(int left, int right, int root, int rootLeft, int rootRight) {
        //查询区间不合法
        if (rootRight < left || rootLeft > right)
            return 0;
        // 当前节点刚好为查询区间,或是查询区间的子集,这时返回该节点所管辖的最小值
        if (left <= rootLeft && right >= rootRight) {
            return min[root];
        } else {
            int m = rootLeft + ((rootRight - rootLeft) >> 1);
            //先设置初始值为极大值,方便比较最小值
            int res = Integer.MAX_VALUE;
            //查询区间与当前节点管理的左区间可能有交集,访问左孩子节点
            if (left <= m) {
                res = Math.min(res, findMin(left, right, root * 2, rootLeft, m));
            }
            //查询区间与当前节点管理的右区间可能有交集,访问右孩子节点
            if (right > m) {
                res = Math.min(res, findMin(left, right, root * 2 + 1, m + 1, rootRight));
            }
            return res;
        }
    }

查询[left,rigtht]中最大值

    /**
     * 查询区间[left,right]中的最大值
     */
    public int queryMax(int left, int right) {
        //查询区间不合法
        if (left > right)
            return -1;
        this.buildMax(1, this.nums.length - 1, 1);
        return this.findMax(left, right, 1, 1, this.nums.length - 1);
    }

    /**
     * @param left 所需查询区间的左边界
     * @param right 所需查询区间的有边界
     * @param root 当前遍历到的区间
     * @param rootLeft root管辖的区间左边界
     * @param rootRight root管辖的区间右边界
     * @return [left,right] 中的最大值
     */
    public int findMax(int left, int right, int root,int rootLeft,int rootRight) {
        //查询区间不合法
        if (rootRight < left || rootLeft > right)
            //先设置初始值为极小值,方便比较最大值
            return Integer.MIN_VALUE;
        // 当前节点刚好为查询区间,或是查询区间的子集,这时返回该节点所管辖的最大值
        if (left <= rootLeft && right >= rootRight) {
            return max[root];
        }
        else {
            int m = rootLeft + ((rootRight - rootLeft) >> 1);
            int res = Integer.MIN_VALUE;
            //查询区间与当前节点管理的左区间可能有交集,访问左孩子节点
            if (left <= m) {
                res = Math.max(res, findMax(left, right, root * 2, rootLeft, m));
            }
            //查询区间与当前节点管理的右区间可能有交集,访问右孩子节点
            if (right > m) {
                res = Math.max(res, findMax(left, right, root * 2 + 1, m + 1, rootRight));
            }
            return res;
        }
    }

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

无懒惰标记

​ 先展示不使用懒惰标记的修改方法,其思想与上面的 [] 类似,只修改包含了目标区间的节点元素。具体的,假设需要修改的区间为 [left,rigth] ,我们需要不停的递归查找符合条件的节点[i,j],直至叶子节点(此时均只包含一个目标区间的元素)然后修改该叶子节点然后停止递归回溯,由于我们可能修改了叶子节点,所以在回溯时需要更新父节点的值。途中如果我们遇到了完全包含于目标区间的节点(left<=i<=j<=right),则直接进行更新:

1)如果是修改区间内所有元素成某个数值:d[]=(j1+1) ,解释:当前节点所管辖的范围均包含于目标区间,修改该区间内的全部元素值后,该区间内的所有的数值都是一样,此时该节点所管辖的总和是可以确定的,即,

2)如果是将区间内所有元素均加上同一值d[]+=(j1+1),解释:由于该区间内全部累加上相同的值,故只需在原来的总和基础上累计上新的所需添加的和(j1+1) 即可

​ 举个栗子,还是用上面的那个图,我要将区间[2,5]内的全部元素修改成5。

  1. 一号节点的左右孩子管辖范围均包含了目标区间,因此对其左右孩子进行递归。
  2. 二号节点的左右孩子管辖范围均包含了目标区间,因此对其左右孩子进行递归。
  3. 四号节点的左孩子没有包含目标区间,故不对其调用;而右孩子反之。
  4. 由于九号孩子的节点完全包含于目标节点,因此对其总和进行更新(2-2+1) * 5 = 5,又由于这是叶子节点,因此停止递归进行回溯。
  5. 每次回溯过程都需要对其节点进行更新(因为可能修改了叶子节点,事实上第四步确实修改了),回溯第四号节点时进行更新第四号节点的总和 = 左孩子总和+右孩子总和= 20+5 = 5 .
  6. 其余过程与上面的一致。。。

image-20220101113301024

image-20220101113323439

区间加上某个值就不演示了,只不过是将直接赋值改成了累加罢了,相信你看了下面的代码就会懂了。

/** 无懒惰标记的修改方法
     * @param sumLeft    规定增加数值的区间左边界(包含于)
     * @param sumRight 规定增加数值的区间右边界(包含于)
     * @param value          规定对区间进行增加的数值
     * @param root         当前遍历到的根节点 root
     * @param rootLeft   当前遍历到的根节点 root  的管辖区间的左边界(包含于)
     * @param rootRight 当前遍历到的根节点 root  的管辖区间的右边界(包含于
     * @param modify      true:表示给[sumLeft,sumRight] 全部修改成value值;false:表示给[sumLeft,sumRight] 全部加上value值;
*/
public void updateNoMark(int sumLeft, int sumRight, int value, int root, int rootLeft, int rootRight,
                         boolean modify) {
    //没有交集的情况
    if (rootRight < sumLeft || rootLeft > sumRight)
        return;
    /**
     * 将所有包含目标查询区间的子集的节点进行相关value的操作
    */

    if (sumLeft <= rootLeft && sumRight >= rootRight) {
        // 进行区间值的累加
        if (!modify) {
            // d[root] 所管辖的数字个数,设为N, 则 N= rootRight - rootLeft + 1 ,其中每个数都需要增加 value 个大小,
            // 故d[root]的总和需要在原来的基础上累加 sum = N*value
            d[root] += (rootRight - rootLeft + 1) * value;
        }
        // 进行区间值的修改
        else {
            // d[root] 所管辖的数字个数,设为N, 则 N= rootRight - rootLeft + 1 ,其中每个数都需要修改成 value
            // 故d[root]需要的总累修改为 N*value
            d[root] = (rootRight - rootLeft + 1) * value;
        }
    }

    //如果为叶子节点,则终止递归,而且放在第二个if后面是为了确保符合条件的叶子节点能顺利修改
    if (rootLeft == rootRight)
        return;
    int m = rootLeft + ((rootRight - rootLeft) >> 1);
    // 如果查询区间与目前遍历到的根节点所管辖的左区间有交集的话,进行左区间遍历
    if (sumLeft <= m)
        updateNoMark(sumLeft, sumRight, value, root * 2, rootLeft, m, modify);
    // 如果查询区间与目前遍历到的根节点所管辖的右区间有交集的话,进行右区间遍历
    if (sumRight > m)
        updateNoMark(sumLeft, sumRight, value, root * 2 + 1, m + 1, rootRight, modify);
    // 更新当前的root节点,因为孩子节点可能更新了数值
    d[root] = d[root * 2] + d[root * 2 + 1];
}
懒惰标记

​ 通过上面的代码演示相信大家都看出来了,修改过程需要将所有包含目标区间子集的节点全部修改,可见这时间复杂度是蛮高的,那么有没有什么办法可以减少其修改时的访问次数呢?又或者说,将不必要的修改延迟做呢?答案是肯定的,这就是接下来要介绍的:懒惰标记。以数组为表现形式,其中t[i]d[i] 一一对应。表示其对该区间所作修改的值的大小,然后还有一个标志数组,flage[] 用来「标记懒惰数组的状态」,记录对应懒惰数组做的是什么类型的操作,是啥都没干还是加值还是置数,这就对用了该数组的三个数值:0,1,-1。初始值为0.

​ 如果不考虑使用flage标记数组而只以t[]的值是否为0来判断是否对区间做了修改的话,当对该区间置0时,下推将会失效。,另一方面,使用一个标记数组可以有效的避免将区间更新操作如求区间和等分开写成两个单独的函数模块。

image-20220101160819586

​ 每次在修改区间之后,将对应的区间打上标记。这时,如果是不带标记的区间修改的话,除非当前是叶子节点,否则还是需要往下递归修改节点(包含目标区间)的,但是我们这个带懒惰标记的算法就不一样了,当他修改完 (见区间查询) 之后便停止递归了。实质上的区间修改将会在下次访问到该节点时进行,并且将标记下放。

​ 举个栗子,假设当前访问到的是极大子区间d[3],所需操作为,置数为5。在修改了d[3]=5之后,将该区间对应的懒惰标记打上标记(所需修改的值) t[3] = 5 (t为懒惰标记数组),flage[3] = -1 之后便停止递归。下次访问(如区间查询)到该节点时发现其 flage[i] != 0,则执行标记下放至左右孩子并且更新左右孩子的数值:

  1. ①更新左孩子的懒惰标记与标记数组,t[3*2] = t[3],flage[3*2] = flage[3]; ②依照上面无懒惰区间修改 方法中的数值更新进行相应的更新。

  2. ①更新右孩子的懒惰标记与标记数组,t[3*2+1] = t[3],flage[3*2+1] = flage[3];② 依照上面无懒惰区间修改 方法中的数值更新进行相应的更新。

需要注意的是,标签下放只会下放給左右孩子,也就是说执行标签下放一次只会走一层而不会继续的递归调用下放标签。这懒惰标记很懒,每次只走一层:D

对区间[2,6]执行加五操作之后的图示:

image-20220101163744521

由上图可知,被作为极大子区间来修改的节点有 3,5,9。而这也正对应了极大子区间的划分,即将查询区间区间划分:[2,6]=[2,2][3,3][4,6] ,这时我们注意到,虽然第三号节点的子孙节点并没有相应的更新,但第三号节点自身被搭上了懒惰标记5,表明所对该区间的修改数值为5,那么具体作何种修改呢?可以从对应的flage中得知,1表明的是加值操作。而关于其子孙节点的修改将在下次造访3号节点时(如区间查询)执行。

某次访问被标记了的三号节点的标签下放示意图:

image-20220101174928054

值得注意的一点是,当标签下放完之后并不需要更新父节点的总和,因为父节点对区间操作完之后的总和在打上懒惰标签之后已经做完了,标签下放只不过是将剩余没完成的部分完成而已。

/**带有懒惰标记的修改方法
     * @param sumLeft    规定增加数值的区间左边界(包含于)
     * @param sumRight 规定增加数值的区间右边界(包含于)
     * @param value          规定对区间进行增加的数值
     * @param root         当前遍历到的根节点 root
     * @param rootLeft   当前遍历到的根节点 root  的管辖区间的左边界(包含于)
     * @param rootRight 当前遍历到的根节点 root  的管辖区间的右边界(包含于
     * @param modify      true:表示给[sumLeft,sumRight] 全部修改成value值;false:表示给[sumLeft,sumRight] 全部加上value值;
*/
public void update(int sumLeft, int sumRight, int value, int root, int rootLeft, int rootRight, boolean modify) {
    //没有交集的情况
    if (rootRight < sumLeft || rootLeft > sumRight)
        return;
    // 当前遍历到的root节点所管辖的区间为要求求和的区间的子集时,将该子集进行标记与对其和进行相关与value的操作,结束此次递归
    //如[3,5]可以划分为[3,3]∪[4,5]
    if (sumLeft <= rootLeft && sumRight >= rootRight) {
        // 进行区间值的累加
        if (!modify) {
            // d[root] 所管辖的数字个数,设为N, 则 N= rootRight - rootLeft + 1 ,其中每个数都需要(更新)增加 value 个大小,
            // 故d[root]需要的总累加和 sum = N*value
            d[root] += (rootRight - rootLeft + 1) * value;
            // 并进行标记:对懒惰数组t所对应的root根进行累加
            t[root] += value;
            flage[root] = 1;
        }
        // 进行区间值的修改
        else {
            // d[root] 所管辖的每一个数字,设为N, 则 N= rootRight - rootLeft + 1 都需要(更新)修改成 value
            // 故d[root]需要的总累修改为 N*value
            d[root] = (rootRight - rootLeft + 1) * value;
            // 并进行标记:对懒惰数组t所对应的root根进行修改
            t[root] = value;
            flage[root] = -1;
        }
        return;
    }
    int m = rootLeft + ((rootRight - rootLeft) >> 1);
    // 如果遍历到的当前根节点的所对应的懒惰数组被标记了非零,或者是被区间置0了(由于t数组粗初始时均为零,所以),则进行标记的下放
    // 值得注意的是,叶子节点[rootLeft==rootRight]的懒惰标记并不需要下放,这是因为叶子节点没有孩子节点
    if (rootLeft != rootRight && flage[root] != 0) {
        pushdown(root, rootLeft, rootRight);
    }
    // 如果查询区间与目前遍历到的根节点所管辖的左区间有交集的话,进行左区间遍历
    if (sumLeft <= m)
        update(sumLeft, sumRight, value, root * 2, rootLeft, m, modify);
    // 如果查询区间与目前遍历到的根节点所管辖的右区间有交集的话,进行右区间遍历
    if (sumRight > m)
        update(sumLeft, sumRight, value, root * 2 + 1, m + 1, rootRight, modify);
    // 更新当前的root节点,因为孩子节点可能更新了数值
    d[root] = d[root * 2] + d[root * 2 + 1];
}

由于懒惰标记的特性,为了防止对还未更新的节点进行区间查询操作,所以需要将没有懒惰标记的区间查询方法进行适当的修改来配合懒惰标记的更新。具体的,在对可能含有目标区间交集的孩子节点进行递归时,先检查当前遍历到的节点的flage的数值是否为零,若不为零则进行标记下放,并更新孩子节点的数值,然后再对其递归查询。具体的看下面代码。

public int getsum(int sumLeft, int sumRight, int root, int rootLeft, int rootRight) {
    //毫无交集
    if (rootRight < sumLeft || rootLeft > sumRight)
        return 0;
    // 当前遍历到的root节点所管辖的区间为要求求和的区间的子集时,将该子集[rootLeft,rootRight]返回
    if (sumLeft <= rootLeft && sumRight >= rootRight)
        return d[root];
    int m = rootLeft + ((rootRight - rootLeft) >> 1), sum = 0;
    // 检查是此时的懒惰标记状态,若存在修改则下放标记,
    if (flage[root] != 0) {
        pushdown(root, rootLeft, rootRight);
    }
    // 如果查询区间与目前遍历到的根节点所管辖的左区间有交集的话,进行左区间遍历
    if (sumLeft <= m)
        sum += getsum(sumLeft, sumRight, root * 2, rootLeft, m);
    // 如果查询区间与目前遍历到的根节点所管辖的右区间有交集的话,进行右区间遍历
    if (sumRight > m)
        sum += getsum(sumLeft, sumRight, root * 2 + 1, m + 1, rootRight);
    return sum;
}

代码汇总

package Java;

public class Segmenttree {
    // 线段树总的管辖长度
    int[] d;
    // 懒惰标记
    int[] t;
    //搭配懒惰标记下放时使用的,其主要功能为,防止区间赋值为0时导致下推失效,以及判断懒惰标记的区间是做了何种操作
    // 有三种状态:0--默认;没有对该节点进行修改;-1--对该节点区间进行置值操作;1--对该节点区间进行加值操作
    short[] flage;
    // 暂时存储目标数组
    int[] nums;
    //储存当前节点所管辖范围内的最大值
    int[] max;
    //储存当前节点所管辖范围内的最小
    int[] min;

    /**
     * @param _nums 代表传进来需要划分的数组
     */
    public Segmenttree(int[] _nums) {
        // 线段树叶子节点可大约计算为 4n
        int len = _nums.length * 4 - 1;
        d = new int[len];
        t = new int[len];
        flage = new short[len];
        max = new int[len];
        min = new int[len];
        this.nums= _nums;
    }

    /**
     * 构建线段树
     * @param left 代表左区间
     * @param right 代表右区间
     * @param rooot 代表当前的根节点(编号)
    */
    public void build(int left, int right, int root) {
        // 如果当前区间范围只有一个元素,则直接将对应的nums元素数组中对应的数字放进根节点中(代表所管辖的做右区间之和)
        if (left == right) {
            d[root] = nums[left];
            return;
        }

        // 取 left 与 right 的中间下标
        int m = left + ((right - left) >> 1);
        // 递归左右区间进行建树:
        // 建造左区间 [left,m] 并且当前的根节点为 root*2
        build(left, m, root * 2);
        // 建造右区间 [m+1,right]并且当前的根节点为 root*2+1
        build(m + 1, right, root * 2 + 1);
        // 由于是递归建树的,所以到这步时,该 root 节点的左(root*2)孩子右孩子(root*2+1)节点都已经建造好了
        // 因此,这时只需要按照线段树的定义,该root节点管辖的是左右孩子的总和,即:d[root] = d[root*2]+d[root*2+1]
        d[root] = d[root * 2] + d[root * 2 + 1];
    }

    /**构建线段树的最大值的相关信息
     * @param left root节点管辖的左边区间
     * @param right root节点管辖的右边区间
     * @param root 当前遍历到的节点
     * @return root节点所管理的最大值
     */
    public int buildMax(int left, int right, int root) {
        //如果是无效的节点,为了防止干扰建树,因此返回极小值
        if (left > right) {
            return Integer.MIN_VALUE;
        }
        if (left == right) {
            //需要确保获得的最值是最新的,因此是使用区间查询线段树中的值,而不是直接赋值为nums[left]的值
            max[root] = this.getsum(left, left, 1, 1, this.nums.length - 1);
            return max[root];
        }
        max[root] = Math.max(buildMax(left, left + ((right - left) >> 1), root * 2),
                buildMax(left + ((right - left) >> 1) + 1, right, root * 2 + 1));
        return max[root];
    }

    /**构建线段树的最小值的相关信息
     * @param left root节点管辖的左边区间
     * @param right root节点管辖的右边区间
     * @param root 当前遍历到的节点
     * @return root节点所管理的最小值
     */
    public int buildMin(int left, int right, int root) {
        //如果是无效的节点,为了防止干扰建树,因此返回极大值
        if (left > right) {
            return Integer.MAX_VALUE;
        }
        if (left == right) {
            min[root] = this.getsum(left, left, 1, 1, this.nums.length - 1);
            return min[root];
        }
        min[root] = Math.min(buildMin(left, left + ((right - left) >> 1), root * 2),
                buildMin(left + ((right - left) >> 1) + 1, right, root * 2 + 1));
        return min[root];
    }

    /**
     * 查询区间[left,right]中的最小值
     */
    public int queryMin(int left, int right) {
        //查询区间不合法
        if (left > right)
            return -1;
        this.buildMin(1, this.nums.length - 1, 1);
        return this.findMin(left, right, 1, 1, this.nums.length - 1);
    }

    /**
     * 查询区间[left,right]中的最大值
     */
    public int queryMax(int left, int right) {
        //查询区间不合法
        if (left > right)
            return -1;
        this.buildMax(1, this.nums.length - 1, 1);
        return this.findMax(left, right, 1, 1, this.nums.length - 1);
    }

    /**
     * @param left 所需查询区间的左边界
     * @param right 所需查询区间的有边界
     * @param root 当前遍历到的区间
     * @param rootLeft root管辖的区间左边界
     * @param rootRight root管辖的区间右边界
     * @return [left,right] 中的最小值
     */
    public int findMin(int left, int right, int root, int rootLeft, int rootRight) {
        //查询区间不合法
        if (rootRight < left || rootLeft > right)
            return 0;
        // 当前节点刚好为查询区间,或是查询区间的子集,这时返回该节点所管辖的最小值
        if (left <= rootLeft && right >= rootRight) {
            return min[root];
        } else {
            int m = rootLeft + ((rootRight - rootLeft) >> 1);
            //先设置初始值为极大值,方便比较最小值
            int res = Integer.MAX_VALUE;
            //查询区间与当前节点管理的左区间可能有交集,访问左孩子节点
            if (left <= m) {
                res = Math.min(res, findMin(left, right, root * 2, rootLeft, m));
            }
            //查询区间与当前节点管理的右区间可能有交集,访问右孩子节点
            if (right > m) {
                res = Math.min(res, findMin(left, right, root * 2 + 1, m + 1, rootRight));
            }
            return res;
        }
    }

    /**
     * @param left 所需查询区间的左边界
     * @param right 所需查询区间的有边界
     * @param root 当前遍历到的区间
     * @param rootLeft root管辖的区间左边界
     * @param rootRight root管辖的区间右边界
     * @return [left,right] 中的最大值
     */
    public int findMax(int left, int right, int root,int rootLeft,int rootRight) {
        //查询区间不合法
        if (rootRight < left || rootLeft > right)
            //先设置初始值为极小值,方便比较最大值
            return Integer.MIN_VALUE;
        // 当前节点刚好为查询区间,或是查询区间的子集,这时返回该节点所管辖的最大值
        if (left <= rootLeft && right >= rootRight) {
            return max[root];
        }
        else {
            int m = rootLeft + ((rootRight - rootLeft) >> 1);
            int res = Integer.MIN_VALUE;
            //查询区间与当前节点管理的左区间可能有交集,访问左孩子节点
            if (left <= m) {
                res = Math.max(res, findMax(left, right, root * 2, rootLeft, m));
            }
            //查询区间与当前节点管理的右区间可能有交集,访问右孩子节点
            if (right > m) {
                res = Math.max(res, findMax(left, right, root * 2 + 1, m + 1, rootRight));
            }
            return res;
        }
    }

    /**
     * 主要思想为,将不可直接求的范围之和分解为线段树中存储好了的管辖极大子集进行求和:如[3,5]->[3,3]+[4,5]
     * 获取 nums 规定区间内的数值总和其范围为:[sumLeft,sumRight]
     * @param sumLeft 规定取值范围的左边界(包含于)
     * @param sumRight 规定取值范围的右边界(包含于)
     * @param root 当前遍历到的根节点 root
     * @param rootLeft 当前遍历到的根节点 root  的管辖区间的左边界(包含于)
     * @param  rootRight 当前遍历到的根节点 root  的管辖区间的右边界(包含于
     * @param return 求区间[sumLeft, sumRight] 的和
     */
    public int getsum(int sumLeft, int sumRight, int root, int rootLeft, int rootRight) {
        //毫无交集
        if (rootRight < sumLeft || rootLeft > sumRight)
            return 0;
        // 当前遍历到的root节点所管辖的区间为要求求和的区间的子集时,将该子集[rootLeft,rootRight]返回
        if (sumLeft <= rootLeft && sumRight >= rootRight)
            return d[root];
        int m = rootLeft + ((rootRight - rootLeft) >> 1), sum = 0;
        // 检查是否还存留着之前修改时的懒惰标记,若有,则下放标记,
        if (flage[root] != 0) {
            pushdown(root, rootLeft, rootRight);
        }
        // 如果查询区间与目前遍历到的根节点所管辖的左区间可能有交集的话,进行左区间遍历
        if (sumLeft <= m)
            sum += getsum(sumLeft, sumRight, root * 2, rootLeft, m);
        // 如果查询区间与目前遍历到的根节点所管辖的右区间可能有交集的话,进行右区间遍历
        if (sumRight > m)
            sum += getsum(sumLeft, sumRight, root * 2 + 1, m + 1, rootRight);
        return sum;
    }

    /**带有懒惰标记的修改方法
     * @param sumLeft    规定增加数值的区间左边界(包含于)
     * @param sumRight 规定增加数值的区间右边界(包含于)
     * @param value          规定对区间进行增加的数值
     * @param root         当前遍历到的根节点 root
     * @param rootLeft   当前遍历到的根节点 root  的管辖区间的左边界(包含于)
     * @param rootRight 当前遍历到的根节点 root  的管辖区间的右边界(包含于
     * @param modify      true:表示给[sumLeft,sumRight] 全部修改成value值;false:表示给[sumLeft,sumRight] 全部加上value值;
     */
    public void update(int sumLeft, int sumRight, int value, int root, int rootLeft, int rootRight, boolean modify) {
        //没有交集的情况
        if (rootRight < sumLeft || rootLeft > sumRight)
            return;
        // 当前遍历到的root节点所管辖的区间为要求求和的区间的子集时,将该子集进行标记与对其和进行相关与value的操作,结束此次递归
        //如[3,5]可以划分为[3,3]∪[4,5]
        if (sumLeft <= rootLeft && sumRight >= rootRight) {
            // 进行区间值的累加
            if (!modify) {
                // d[root] 所管辖的数字个数,设为N, 则 N= rootRight - rootLeft + 1 ,其中每个数都需要(更新)增加 value 个大小,
                // 故d[root]需要的总累加和 sum = N*value
                d[root] += (rootRight - rootLeft + 1) * value;
                // 并进行标记:对懒惰数组t所对应的root根进行累加
                t[root] += value;
                flage[root] = 1;
            }
            // 进行区间值的修改
            else {
                // d[root] 所管辖的每一个数字,设为N, 则 N= rootRight - rootLeft + 1 都需要(更新)修改成 value
                // 故d[root]需要的总累修改为 N*value
                d[root] = (rootRight - rootLeft + 1) * value;
                // 并进行标记:对懒惰数组t所对应的root根进行修改
                t[root] = value;
                flage[root] = -1;
            }
            return;
        }
        int m = rootLeft + ((rootRight - rootLeft) >> 1);
        // 如果遍历到的当前根节点的所对应的懒惰数组被标记了非零,或者是被区间置0了(由于t数组粗初始时均为零,所以),则进行标记的下放
        // 值得注意的是,叶子节点[rootLeft==rootRight]的懒惰标记并不需要下放,这是因为叶子节点没有孩子节点
        if (rootLeft != rootRight && flage[root] != 0) {
            pushdown(root, rootLeft, rootRight);
        }
        // 如果查询区间与目前遍历到的根节点所管辖的左区间有交集的话,进行左区间遍历
        if (sumLeft <= m)
            update(sumLeft, sumRight, value, root * 2, rootLeft, m, modify);
        // 如果查询区间与目前遍历到的根节点所管辖的右区间有交集的话,进行右区间遍历
        if (sumRight > m)
            update(sumLeft, sumRight, value, root * 2 + 1, m + 1, rootRight, modify);
        // 更新当前的root节点,因为孩子节点可能更新了数值
        d[root] = d[root * 2] + d[root * 2 + 1];
    }

    /** 无懒惰标记的修改方法
     * @param sumLeft    规定增加数值的区间左边界(包含于)
     * @param sumRight 规定增加数值的区间右边界(包含于)
     * @param value          规定对区间进行增加的数值
     * @param root         当前遍历到的根节点 root
     * @param rootLeft   当前遍历到的根节点 root  的管辖区间的左边界(包含于)
     * @param rootRight 当前遍历到的根节点 root  的管辖区间的右边界(包含于
     * @param modify      true:表示给[sumLeft,sumRight] 全部修改成value值;false:表示给[sumLeft,sumRight] 全部加上value值;
     */
    public void updateNoMark(int sumLeft, int sumRight, int value, int root, int rootLeft, int rootRight,
            boolean modify) {
        //没有交集的情况
        if (rootRight < sumLeft || rootLeft > sumRight)
            return;
        /**
         * 将所有包含目标查询区间的子集的节点进行相关value的操作
         */

        if (sumLeft <= rootLeft && sumRight >= rootRight) {
            // 进行区间值的累加
            if (!modify) {
                // d[root] 所管辖的数字个数,设为N, 则 N= rootRight - rootLeft + 1 ,其中每个数都需要增加 value 个大小,
                // 故d[root]的总和需要在原来的基础上累加 sum = N*value
                d[root] += (rootRight - rootLeft + 1) * value;
            }
            // 进行区间值的修改
            else {
                // d[root] 所管辖的数字个数,设为N, 则 N= rootRight - rootLeft + 1 ,其中每个数都需要修改成 value
                // 故d[root]需要的总累修改为 N*value
                d[root] = (rootRight - rootLeft + 1) * value;
            }
        }

        //如果为叶子节点,则终止递归,而且放在第二个if后面是为了确保符合条件的叶子节点能顺利修改
        if (rootLeft == rootRight)
            return;
        int m = rootLeft + ((rootRight - rootLeft) >> 1);
        // 如果查询区间与目前遍历到的根节点所管辖的左区间有交集的话,进行左区间遍历
        if (sumLeft <= m)
            updateNoMark(sumLeft, sumRight, value, root * 2, rootLeft, m, modify);
        // 如果查询区间与目前遍历到的根节点所管辖的右区间有交集的话,进行右区间遍历
        if (sumRight > m)
            updateNoMark(sumLeft, sumRight, value, root * 2 + 1, m + 1, rootRight, modify);
        // 更新当前的root节点,因为孩子节点可能更新了数值
        d[root] = d[root * 2] + d[root * 2 + 1];
    }

    /**
     * 下放方法:
     * 对其左孩子[root*2]右孩子[root*2+1]进行对应的t标记:t[该孩子的编号]+父节点中的标记元素(t[root]) ;
     * 以及其孩子节点的d数组累加;并且取消当前root节点的懒惰标记。
     * @param root          当前遍历到的根节点
     * @param rootLeft    当前遍历到的根节点 root  的管辖区间的左边界(包含于)
     * @param rootRight 当前遍历到的根节点 root  的管辖区间的右边界(包含于)
     * @param modify     true:表示给[sumLeft,sumRight] 全部修改成value值;false:表示给[sumLeft,sumRight] 全部加上value值;
     */
    public void pushdown(int root, int rootLeft, int rootRight) {
        int m = rootLeft + ((rootRight - rootLeft) >> 1);
        // 进行区间值的累加下放
        if (flage[root] == 1) {
            //给左孩子标记
            t[root * 2] += t[root];
            flage[root * 2] = flage[root];
            //给右孩子标记
            t[root * 2 + 1] += t[root];
            flage[root * 2 + 1] = flage[root];
            d[root * 2] += (m - rootLeft + 1) * t[root];//对左孩子对应的d数组进行累加
            d[root * 2 + 1] += (rootRight - m) * t[root];//对右孩子对应的d数组进行累加
        }
        // 进行区间值的修改的下放
        else if (flage[root] == -1) {
            //给左孩子标记
            t[root * 2] = t[root];
            flage[root * 2] = flage[root];
            //给右孩子标记
            t[root * 2 + 1] = t[root];
            flage[root * 2 + 1] = flage[root];
            d[root * 2] = (m - rootLeft + 1) * t[root];//对左孩子对应的d数组进行累加
            d[root * 2 + 1] = (rootRight - m) * t[root];//对右孩子对应的d数组进行累加
        }
        t[root] = 0;//取消当前root节点的懒惰标记
        flage[root] = 0;//取消当前root节点的修改标记
    }

    /**
    * 测试
    */
    public static void main(String[] a) {

        Segmenttree st3 = new Segmenttree(new int[] { 0, 20, 21, 22, 23, 24, 25 });
        st3.build(1, st3.nums.length - 1, 1);//左右边界要与传入的数组的下标一致。故 从0 到 length-1
        System.out.println(st3.queryMin(1, 6));
        System.out.println(st3.queryMax(5, 6));
        System.out.print(st3.getsum(-4, -1, 1, 1, st3.nums.length - 1));

        System.out.println("\n---将[2,6]全部加上5-----");
        System.out.println("\n---修改前-----");
        for (int num : st3.d) {
            System.out.print("  " + num);
        }

        System.out.println("\n----修改后----");
        st3.update(2, 6, 5, 1, 1, st3.nums.length - 1, false);
        System.out.println("\n----第一次:----");
        for (int num : st3.d) {
            System.out.print("  " + num);
        }
        System.out.println("\n----强制访问[5,5]使标签下放后的结果:----");
        st3.getsum(5, 5, 1, 1, st3.nums.length - 1);
        for (int num : st3.d) {
            System.out.print("  " + num);
        }
        System.out.println("END");
    }
}
posted @   Mercurows  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示