线段树:主要是用于高效解决区间查询和更新的问题,它通过分治思想和用空间换取时间的方法,将数组区间进行递归二分,使其构成一颗逻辑上的二叉树,最后使得对区间范围的操作能够在对数时间O(logN)内完成。
1、算法概述
-
线段树:主要是用于高效解决区间查询和更新的问题,它通过分治思想和用空间换取时间的方法,将数组区间进行递归二分,使其构成一颗逻辑上的二叉树,
-
最后使得对区间范围的操作能够在对数时间O(logN)内完成。
-
线段树解决的问题范畴:大范围信息可以只由左、右两侧信息加工出,而不必遍历左右两个子范围的具体状况
-
线段树的操作思想:
- 1)构建线段树的思想:用线段树解决问题的思想本质上就是把原来的数组当做一个二叉树的叶子节点,然后根据范围构建上层的节点,
- 这样使得新建出来的线段树数组的根节点能够表示整个区间,二分之后的位置能够表示它所包含的区间,这样一层一层下来,就达到了缓存的效果。
- 因为数组能够利用下标直接表示一颗二叉树,所以我们只需要空充一下原来数组的大小,就可以完整的表示出一个逻辑上的二叉树。
- 2)延迟更新(懒更新)的思想:线段树的另一个比较重要的思想就是延迟更新,我们在构建线段树的时候,已经用范围将原来的数组进行了扩充。这样如果在后续操作的时候,
- 如果要操作的范围已经完全包含了二叉树的某一个节点的范围,这样我们就可以直接用缓存计算出这里的结果,而不需要再往下拆分,除非到不得不进行拆分的时候。
- 当下一次操作到该位置的时候,我们先查看之前有没有延迟更新的操作,有的话就先下发执行,然后继续延迟当前的操作。
-
通过这种方式,我们就将原来要立即执行的操作进行了延迟,节省了时间。
-
3)空间换时间的思想:线段树操作是用空间换时间的方法来的,其构成的线段树数组必然占用更多的空间。
- 4)构建的线段树的数组的多少,是根据题目的要求来定的,并不是统一的,根据不同题目的要求,可以加不同的方法,但是一般都会有查询的方法。
-
几个重要的参数:
- 1)线段树的二叉树下标表示方式:从1开始,左子节点下标:2i,右子节点下标:2i+1,父节点下标:i/2
- 用数组来表示二叉树,有两种表示方法:
- 一种是起始索引为0,左子节点下标为:2i+1,右子节点下标为:2i+2,父节点下标为:(i-1)/2;
- 另一种是起始索引为1,左子节点下标:2i,右子节点下标:2i+1,父节点下标:i/2
- 因为在线段树中要频繁计算父子节点的索引,所以我们直接用起始索引为1的方法,因为这个方法可以直接用位运算来计算,效率比较高。
- 2)线段树数组的大小:一般为4*n
- 线段树在逻辑上是一棵完全二叉树,但在用数组存储时,我们通常按照"根节点为1,左孩子为2i,右孩子为2i+1"的规则进行编号(假设数组下标从1开始)。
- 当原序列长度n不是2的整数次幂时,构建的线段树不是一棵满二叉树,其最后一层的节点会分散在数组的不同位置。
- 最消耗空间的情况发生在 n的值刚好超过某个2的整数次幂时(例如 n = 2^k + 1)。
- 在这种情况下,线段树需要几乎两倍的叶子节点空间,再加上所有中间节点,总的节点数在最坏情况下会接近 4n - 1。
- 因此,分配 4 * n的空间是一个安全且通用的经验值,可以确保在任何情况下都不会发生数组越界。
2、同时支持范围加和更新的线段树实现
- 同时支持范围加和更新的线段树实现
- 思路:
- 要同时支持范围内的加和更新,还要支持查询某个范围内的累加和。
- 根据线段树的思想,我们需要如下的线段数组:
- 1)sum数组,用来统计各个空间的累加和,供随时查询和增加使用。
- 2)lazy数组,用来标记当前位置有没有已经存在累加和的懒更新,有值表示存在累加的懒更新,为0表示没有
- 3)update数组,用来区分lazy的位置的值是否为更新操作,为1表示是更新操作,0表示是增加操作。
- 4)change数组,用来记录当前位置懒更新的值。
- sum数组是直接放结果的,lazy、update、change数组是为了懒更新准备的。
- 其中lazy是为了累加和的懒更新准备的,update和change是为了更新操作的懒更新准备的。
- 操作的流程如下:
- 1)在新建的时候,我们根据传入的原始数组进行构建线段树的sum数组。
- 2)在执行add和update操作时,我们根据范围下发操作,到下发的范围已经包含了当前线段树节点代表的范围的时候,我们先查看有没有前面的懒更新,
- 如果有,先下发执行之前的懒更新,然后我们直接更新这个范围的sum值,将下发操作在这个位置懒住,不再下发,
- 在这种情况下,在这个范围上的查询是没有问题的,但是如果在这个位置往下要继续执行其他操作的时候,因为下面的节点的操作懒住了,所以下面的节点
- 是错误的,需要先执行这个懒住的操作,然后继续执行需要的操作。
java
/**
* 同时支持范围加和更新的线段树实现
* 思路:
* 要同时支持范围内的加和更新,还要支持查询某个范围内的累加和。
* 根据线段树的思想,我们需要如下的线段数组:
* 1)sum数组,用来统计各个空间的累加和,供随时查询和增加使用。
* 2)lazy数组,用来标记当前位置有没有已经存在累加和的懒更新,有值表示存在累加的懒更新,为0表示没有
* 3)update数组,用来区分lazy的位置的值是否为更新操作,为1表示是更新操作,0表示是增加操作。
* 4)change数组,用来记录当前位置懒更新的值。
* sum数组是直接放结果的,lazy、update、change数组是为了懒更新准备的。
* 其中lazy是为了累加和的懒更新准备的,update和change是为了更新操作的懒更新准备的。
* 操作的流程如下:
* 1)在新建的时候,我们根据传入的原始数组进行构建线段树的sum数组。
* 2)在执行add和update操作时,我们根据范围下发操作,到下发的范围已经包含了当前线段树节点代表的范围的时候,我们先查看有没有前面的懒更新,
* 如果有,先下发执行之前的懒更新,然后我们直接更新这个范围的sum值,将下发操作在这个位置懒住,不再下发,
* 在这种情况下,在这个范围上的查询是没有问题的,但是如果在这个位置往下要继续执行其他操作的时候,因为下面的节点的操作懒住了,所以下面的节点
* 是错误的,需要先执行这个懒住的操作,然后继续执行需要的操作。
*/
public static class AddUpdateSegmentTree {
// 用来记录右侧边界,左边界为1,这个边界不是线段树数组的右侧下标,是初始设置的时候的原来的右侧边界
private int rightBoard;
// sum[]:模拟线段树维护区间和
private int[] sum;
//lazy[]为累加和懒惰标记,有值表示存在累加的懒更新,为0表示没有
private int[] lazy;
// change[]为更新的值
private int[] change;
// update[]为更新慵懒标记,1表示是更新操作,0表示是增加操作。
private boolean[] update;
public AddUpdateSegmentTree(int[] origin) {
int n = origin.length;
// 原始数组的有边界从0到n-1,我们从1开始算的话,就是从1到n
this.rightBoard = n;
// 以 4*n 的大小来新建数组
this.sum = new int[n << 2];
this.lazy = new int[n << 2];
this.change = new int[n << 2];
this.update = new boolean[n << 2];
// origin从0开始,我们新建一个从1开始的数组,用来构建sum数组
int[] arr = new int[n + 1];
for (int i = 1; i < arr.length; i++) {
arr[i] = origin[i - 1];
}
// 初始化构建sum数组
build(1, n, 1, arr);
}
/**
* 查询L..R范围上的累加和
*/
public long query(int L, int R) {
// 在整个范围上查找
return query(L, R, 1, this.rightBoard, 1);
}
/**
* 在leftBoard到rightBoard的范围查询包含在L..R上的累加和
* L和R是想要更新的范围边界
* leftBoard和rightBoard是线段树二分的左右边界
* i是sum数组的下标位置,
* 也就是说leftBoard和rightBoard是i下标对应的范围
*/
private long query(int L, int R, int leftBoard, int rightBoard, int i) {
// 全包含,直接返回
if (L <= leftBoard && rightBoard <= R) {
return sum[i];
}
// 没有全包含,先清理懒操作,然后左右查找
int mid = (leftBoard + rightBoard) >> 1;
pushDown(i, mid - leftBoard + 1, rightBoard - mid);
long ans = 0;
if (L <= mid) {
ans += query(L, R, leftBoard, mid, i << 1);
}
if (R > mid) {
ans += query(L, R, mid + 1, rightBoard, i << 1 | 1);
}
return ans;
}
/**
* 更新L..R范围的值为C
*/
public void update(int L, int R, int C) {
// 从整个边界开始更新
update(L, R, C, 1, this.rightBoard, 1);
}
/**
* 更新L..R范围的值为C,
* L和R是想要更新的范围边界
* leftBoard和rightBoard是线段树二分的左右边界
* i是sum数组的下标位置,
* 也就是说leftBoard和rightBoard是i下标对应的范围
*/
private void update(int L, int R, int C, int leftBoard, int rightBoard, int i) {
// 想要更新的范围L..R全部包含了目前的边界leftBoard..rightBoard,将这个位置懒更新
if (L <= leftBoard && rightBoard <= R) {
update[i] = true;
change[i] = C;
sum[i] = C * (rightBoard - leftBoard + 1);
// 以前累计的累加更新不需要了,直接清空
lazy[i] = 0;
return;
}
// 当前任务躲不掉,无法懒更新,要继续往下发,
// 先将当前位置的懒操作往下发
int mid = (leftBoard + rightBoard) >> 1;
pushDown(i, mid - leftBoard + 1, rightBoard - mid);
// 往左右两侧进行尝试
if (L <= mid) {
update(L, R, C, leftBoard, mid, i << 1);
}
if (R > mid) {
update(L, R, C, mid + 1, rightBoard, i << 1 | 1);
}
// 操作完左右子树后,更新当前位置
pushUp(i);
}
/**
* L..R范围每个值加上C
*/
public void add(int L, int R, int C) {
// 从整个边界开始尝试
add(L, R, C, 1, this.rightBoard, 1);
}
/**
* L..R范围每个值加上C
* L和R是想要增加的范围边界
* leftBoard和rightBoard是线段树二分的左右边界
* i是sum数组的下标位置,
* 也就是说leftBoard和rightBoard是i下标对应的范围
*/
private void add(int L, int R, int C, int leftBoard, int rightBoard, int i) {
// 当前尝试范围被全包了,懒增加
if (L <= leftBoard && rightBoard <= R) {
sum[i] += C * (rightBoard - leftBoard + 1);
lazy[i] += C;
return;
}
// 任务没有全包,尝试左右边界
int mid = (leftBoard + rightBoard) >> 1;
// 先把前面的懒操作清了
pushDown(i, mid - leftBoard + 1, rightBoard - mid);
// 尝试左右子树
if (L <= mid) {
add(L, R, C, leftBoard, mid, i << 1);
}
if (R > mid) {
add(L, R, C, mid + 1, rightBoard, i << 1 | 1);
}
// 操作完左右子树后,更新当前位置
pushUp(i);
}
/**
* 在初始化阶段构建sum数组
* 在arr[l~r]范围上,去build,1~n,
* i : 这个范围在sum中的下标
*/
private void build(int l, int r, int i, int[] arr) {
// 到叶子节点,和就是其本身
if (l == r) {
sum[i] = arr[l];
return;
}
// 不是叶子节点,需要先构建下面的节点,然后计算出当前节点
// 中间节点的下标 : mid=(l+r)/2
int mid = (l + r) >> 1;
// 构建左子节点,2*i
build(l, mid, i << 1, arr);
// 构建右子节点,i << 1 | 1 = i * 2 + 1
build(mid + 1, r, i << 1 | 1, arr);
// 根据左右子节点,计算当前节点的和
pushUp(i);
}
/**
* 根据左右子节点计算当前节点的和
* i :要求的当前节点的下标
*/
private void pushUp(int i) {
// sum[i] = sum[2*i] + sum[2*1+1]
sum[i] = sum[i << 1] + sum[i << 1 | 1];
}
/**
* 之前的,所有懒增加,和懒更新,从父范围,发给左右两个子范围
*
* @param i :当前节点的下标
* @param leftCount :左子树的节点个数
* @param rightCount : 右子树的节点个数
*/
public void pushDown(int i, int leftCount, int rightCount) {
// 处理更新操作的懒更新
if (update[i]) {
// 推到左侧
update[i << 1] = true;
change[i << 1] = change[i];
// 推到右侧
update[i << 1 | 1] = true;
change[i << 1 | 1] = change[i];
// 已经更新了,就不需要记录原来的累加和的操作了,直接清空左右子树的累加和懒更新
lazy[i << 1] = 0;
lazy[i << 1 | 1] = 0;
// 求左右两个节点的和
sum[i << 1] = change[i] * leftCount;
sum[i << 1 | 1] = change[i] * rightCount;
// 清楚懒更新标记
update[i] = false;
}
// 处理累加操作的懒更新,
if (lazy[i] != 0) {
// 更新左子树
lazy[i << 1] += lazy[i];
sum[i << 1] += lazy[i] * leftCount;
// 更新右子树
lazy[i << 1 | 1] += lazy[i];
sum[i << 1 | 1] += lazy[i] * rightCount;
// 清空标记
lazy[i] = 0;
}
}
}
整体代码和测试如下:
java
/**
* 线段树:主要是用于高效解决区间查询和更新的问题,它通过分治思想和用空间换取时间的方法,将数组区间进行递归二分,使其构成一颗逻辑上的二叉树,
* 最后使得对区间范围的操作能够在对数时间O(logN)内完成。
* 线段树解决的问题范畴:大范围信息可以只由左、右两侧信息加工出,而不必遍历左右两个子范围的具体状况
* <br>
* 线段树的操作思想:
* 1)构建线段树的思想:用线段树解决问题的思想本质上就是把原来的数组当做一个二叉树的叶子节点,然后根据范围构建上层的节点,
* 这样使得新建出来的线段树数组的根节点能够表示整个区间,二分之后的位置能够表示它所包含的区间,这样一层一层下来,就达到了缓存的效果。
* 因为数组能够利用下标直接表示一颗二叉树,所以我们只需要空充一下原来数组的大小,就可以完整的表示出一个逻辑上的二叉树。
* 2)延迟更新(懒更新)的思想:线段树的另一个比较重要的思想就是延迟更新,我们在构建线段树的时候,已经用范围将原来的数组进行了扩充。这样如果在后续操作的时候,
* 如果要操作的范围已经完全包含了二叉树的某一个节点的范围,这样我们就可以直接用缓存计算出这里的结果,而不需要再往下拆分,除非到不得不进行拆分的时候。
* 当下一次操作到该位置的时候,我们先查看之前有没有延迟更新的操作,有的话就先下发执行,然后继续延迟当前的操作。
* 通过这种方式,我们就将原来要立即执行的操作进行了延迟,节省了时间。
* 3)空间换时间的思想:线段树操作是用空间换时间的方法来的,其构成的线段树数组必然占用更多的空间。
* 4)构建的线段树的数组的多少,是根据题目的要求来定的,并不是统一的,根据不同题目的要求,可以加不同的方法,但是一般都会有查询的方法。
* <br>
* 几个重要的参数:
* 1)线段树的二叉树下标表示方式:从1开始,左子节点下标:2*i,右子节点下标:2*i+1,父节点下标:i/2
* 用数组来表示二叉树,有两种表示方法:
* 一种是起始索引为0,左子节点下标为:2*i+1,右子节点下标为:2*i+2,父节点下标为:(i-1)/2;
* 另一种是起始索引为1,左子节点下标:2*i,右子节点下标:2*i+1,父节点下标:i/2
* 因为在线段树中要频繁计算父子节点的索引,所以我们直接用起始索引为1的方法,因为这个方法可以直接用位运算来计算,效率比较高。
* 2)线段树数组的大小:一般为4*n
* 线段树在逻辑上是一棵完全二叉树,但在用数组存储时,我们通常按照"根节点为1,左孩子为2i,右孩子为2i+1"的规则进行编号(假设数组下标从1开始)。
* 当原序列长度n不是2的整数次幂时,构建的线段树不是一棵满二叉树,其最后一层的节点会分散在数组的不同位置。
* 最消耗空间的情况发生在 n的值刚好超过某个2的整数次幂时(例如 n = 2^k + 1)。
* 在这种情况下,线段树需要几乎两倍的叶子节点空间,再加上所有中间节点,总的节点数在最坏情况下会接近 4n - 1。
* 因此,分配 4 * n的空间是一个安全且通用的经验值,可以确保在任何情况下都不会发生数组越界。
*
*/
public class SegmentTree {
/**
* 同时支持范围加和更新的线段树实现
* 思路:
* 要同时支持范围内的加和更新,还要支持查询某个范围内的累加和。
* 根据线段树的思想,我们需要如下的线段数组:
* 1)sum数组,用来统计各个空间的累加和,供随时查询和增加使用。
* 2)lazy数组,用来标记当前位置有没有已经存在累加和的懒更新,有值表示存在累加的懒更新,为0表示没有
* 3)update数组,用来区分lazy的位置的值是否为更新操作,为1表示是更新操作,0表示是增加操作。
* 4)change数组,用来记录当前位置懒更新的值。
* sum数组是直接放结果的,lazy、update、change数组是为了懒更新准备的。
* 其中lazy是为了累加和的懒更新准备的,update和change是为了更新操作的懒更新准备的。
* 操作的流程如下:
* 1)在新建的时候,我们根据传入的原始数组进行构建线段树的sum数组。
* 2)在执行add和update操作时,我们根据范围下发操作,到下发的范围已经包含了当前线段树节点代表的范围的时候,我们先查看有没有前面的懒更新,
* 如果有,先下发执行之前的懒更新,然后我们直接更新这个范围的sum值,将下发操作在这个位置懒住,不再下发,
* 在这种情况下,在这个范围上的查询是没有问题的,但是如果在这个位置往下要继续执行其他操作的时候,因为下面的节点的操作懒住了,所以下面的节点
* 是错误的,需要先执行这个懒住的操作,然后继续执行需要的操作。
*/
public static class AddUpdateSegmentTree {
// 用来记录右侧边界,左边界为1,这个边界不是线段树数组的右侧下标,是初始设置的时候的原来的右侧边界
private int rightBoard;
// sum[]:模拟线段树维护区间和
private int[] sum;
//lazy[]为累加和懒惰标记,有值表示存在累加的懒更新,为0表示没有
private int[] lazy;
// change[]为更新的值
private int[] change;
// update[]为更新慵懒标记,1表示是更新操作,0表示是增加操作。
private boolean[] update;
public AddUpdateSegmentTree(int[] origin) {
int n = origin.length;
// 原始数组的有边界从0到n-1,我们从1开始算的话,就是从1到n
this.rightBoard = n;
// 以 4*n 的大小来新建数组
this.sum = new int[n << 2];
this.lazy = new int[n << 2];
this.change = new int[n << 2];
this.update = new boolean[n << 2];
// origin从0开始,我们新建一个从1开始的数组,用来构建sum数组
int[] arr = new int[n + 1];
for (int i = 1; i < arr.length; i++) {
arr[i] = origin[i - 1];
}
// 初始化构建sum数组
build(1, n, 1, arr);
}
/**
* 查询L..R范围上的累加和
*/
public long query(int L, int R) {
// 在整个范围上查找
return query(L, R, 1, this.rightBoard, 1);
}
/**
* 在leftBoard到rightBoard的范围查询包含在L..R上的累加和
* L和R是想要更新的范围边界
* leftBoard和rightBoard是线段树二分的左右边界
* i是sum数组的下标位置,
* 也就是说leftBoard和rightBoard是i下标对应的范围
*/
private long query(int L, int R, int leftBoard, int rightBoard, int i) {
// 全包含,直接返回
if (L <= leftBoard && rightBoard <= R) {
return sum[i];
}
// 没有全包含,先清理懒操作,然后左右查找
int mid = (leftBoard + rightBoard) >> 1;
pushDown(i, mid - leftBoard + 1, rightBoard - mid);
long ans = 0;
if (L <= mid) {
ans += query(L, R, leftBoard, mid, i << 1);
}
if (R > mid) {
ans += query(L, R, mid + 1, rightBoard, i << 1 | 1);
}
return ans;
}
/**
* 更新L..R范围的值为C
*/
public void update(int L, int R, int C) {
// 从整个边界开始更新
update(L, R, C, 1, this.rightBoard, 1);
}
/**
* 更新L..R范围的值为C,
* L和R是想要更新的范围边界
* leftBoard和rightBoard是线段树二分的左右边界
* i是sum数组的下标位置,
* 也就是说leftBoard和rightBoard是i下标对应的范围
*/
private void update(int L, int R, int C, int leftBoard, int rightBoard, int i) {
// 想要更新的范围L..R全部包含了目前的边界leftBoard..rightBoard,将这个位置懒更新
if (L <= leftBoard && rightBoard <= R) {
update[i] = true;
change[i] = C;
sum[i] = C * (rightBoard - leftBoard + 1);
// 以前累计的累加更新不需要了,直接清空
lazy[i] = 0;
return;
}
// 当前任务躲不掉,无法懒更新,要继续往下发,
// 先将当前位置的懒操作往下发
int mid = (leftBoard + rightBoard) >> 1;
pushDown(i, mid - leftBoard + 1, rightBoard - mid);
// 往左右两侧进行尝试
if (L <= mid) {
update(L, R, C, leftBoard, mid, i << 1);
}
if (R > mid) {
update(L, R, C, mid + 1, rightBoard, i << 1 | 1);
}
// 操作完左右子树后,更新当前位置
pushUp(i);
}
/**
* L..R范围每个值加上C
*/
public void add(int L, int R, int C) {
// 从整个边界开始尝试
add(L, R, C, 1, this.rightBoard, 1);
}
/**
* L..R范围每个值加上C
* L和R是想要增加的范围边界
* leftBoard和rightBoard是线段树二分的左右边界
* i是sum数组的下标位置,
* 也就是说leftBoard和rightBoard是i下标对应的范围
*/
private void add(int L, int R, int C, int leftBoard, int rightBoard, int i) {
// 当前尝试范围被全包了,懒增加
if (L <= leftBoard && rightBoard <= R) {
sum[i] += C * (rightBoard - leftBoard + 1);
lazy[i] += C;
return;
}
// 任务没有全包,尝试左右边界
int mid = (leftBoard + rightBoard) >> 1;
// 先把前面的懒操作清了
pushDown(i, mid - leftBoard + 1, rightBoard - mid);
// 尝试左右子树
if (L <= mid) {
add(L, R, C, leftBoard, mid, i << 1);
}
if (R > mid) {
add(L, R, C, mid + 1, rightBoard, i << 1 | 1);
}
// 操作完左右子树后,更新当前位置
pushUp(i);
}
/**
* 在初始化阶段构建sum数组
* 在arr[l~r]范围上,去build,1~n,
* i : 这个范围在sum中的下标
*/
private void build(int l, int r, int i, int[] arr) {
// 到叶子节点,和就是其本身
if (l == r) {
sum[i] = arr[l];
return;
}
// 不是叶子节点,需要先构建下面的节点,然后计算出当前节点
// 中间节点的下标 : mid=(l+r)/2
int mid = (l + r) >> 1;
// 构建左子节点,2*i
build(l, mid, i << 1, arr);
// 构建右子节点,i << 1 | 1 = i * 2 + 1
build(mid + 1, r, i << 1 | 1, arr);
// 根据左右子节点,计算当前节点的和
pushUp(i);
}
/**
* 根据左右子节点计算当前节点的和
* i :要求的当前节点的下标
*/
private void pushUp(int i) {
// sum[i] = sum[2*i] + sum[2*1+1]
sum[i] = sum[i << 1] + sum[i << 1 | 1];
}
/**
* 之前的,所有懒增加,和懒更新,从父范围,发给左右两个子范围
*
* @param i :当前节点的下标
* @param leftCount :左子树的节点个数
* @param rightCount : 右子树的节点个数
*/
public void pushDown(int i, int leftCount, int rightCount) {
// 处理更新操作的懒更新
if (update[i]) {
// 推到左侧
update[i << 1] = true;
change[i << 1] = change[i];
// 推到右侧
update[i << 1 | 1] = true;
change[i << 1 | 1] = change[i];
// 已经更新了,就不需要记录原来的累加和的操作了,直接清空左右子树的累加和懒更新
lazy[i << 1] = 0;
lazy[i << 1 | 1] = 0;
// 求左右两个节点的和
sum[i << 1] = change[i] * leftCount;
sum[i << 1 | 1] = change[i] * rightCount;
// 清楚懒更新标记
update[i] = false;
}
// 处理累加操作的懒更新,
if (lazy[i] != 0) {
// 更新左子树
lazy[i << 1] += lazy[i];
sum[i << 1] += lazy[i] * leftCount;
// 更新右子树
lazy[i << 1 | 1] += lazy[i];
sum[i << 1 | 1] += lazy[i] * rightCount;
// 清空标记
lazy[i] = 0;
}
}
}
/**
* 用暴力方法实现的对数器
*/
public static class Comparator {
public int[] arr;
public Comparator(int[] origin) {
arr = new int[origin.length + 1];
for (int i = 0; i < origin.length; i++) {
arr[i + 1] = origin[i];
}
}
public void update(int L, int R, int C) {
for (int i = L; i <= R; i++) {
arr[i] = C;
}
}
public void add(int L, int R, int C) {
for (int i = L; i <= R; i++) {
arr[i] += C;
}
}
public long query(int L, int R) {
long ans = 0;
for (int i = L; i <= R; i++) {
ans += arr[i];
}
return ans;
}
}
public static void main(String[] args) {
int len = 100;
int max = 1000;
int testTimes = 5000;
int addOrUpdateTimes = 1000;
int queryTimes = 500;
System.out.println("开始测试!");
for (int i = 0; i < testTimes; i++) {
int[] origin = genarateRandomArray(len, max);
AddUpdateSegmentTree seg = new AddUpdateSegmentTree(origin);
int N = origin.length;
Comparator rig = new Comparator(origin);
for (int j = 0; j < addOrUpdateTimes; j++) {
int num1 = (int) (Math.random() * N) + 1;
int num2 = (int) (Math.random() * N) + 1;
int L = Math.min(num1, num2);
int R = Math.max(num1, num2);
int C = (int) (Math.random() * max) - (int) (Math.random() * max);
if (Math.random() < 0.5) {
seg.add(L, R, C);
rig.add(L, R, C);
} else {
seg.update(L, R, C);
rig.update(L, R, C);
}
}
for (int k = 0; k < queryTimes; k++) {
int num1 = (int) (Math.random() * N) + 1;
int num2 = (int) (Math.random() * N) + 1;
int L = Math.min(num1, num2);
int R = Math.max(num1, num2);
long ans1 = seg.query(L, R);
long ans2 = rig.query(L, R);
if (ans1 != ans2) {
System.out.println("查询错误!");
System.out.printf("segmentAns:%d,comparatorAns:%d\n", ans1, ans2);
break;
}
}
}
System.out.println("结束测试!");
}
public static int[] genarateRandomArray(int len, int max) {
int size = (int) (Math.random() * len) + 1;
int[] origin = new int[size];
for (int i = 0; i < size; i++) {
origin[i] = (int) (Math.random() * max) - (int) (Math.random() * max);
}
return origin;
}
}
3、题目:掉落的方块
-
题目:掉落的方块
-
想象一下标准的俄罗斯方块游戏,X轴是积木最终下落到底的轴线
-
下面是这个游戏的简化版:
-
1)只会下落正方形积木
-
2)[a,b] -> 代表一个边长为b的正方形积木,积木左边缘沿着X = a这条线从上方掉落
-
3)认为整个X轴都可能接住积木,也就是说简化版游戏是没有整体的左右边界的
-
4)没有整体的左右边界,所以简化版游戏不会消除积木,因为不会有哪一层被填满。
-
给定一个N*2的二维数组matrix,可以代表N个积木依次掉落,
-
返回每一次掉落之后的最大高度
-
线段树的解法
-
思路:
- 本题目是一个一个的正方形往下落,[a,b] 代表一个边长为b的正方形积木,积木左边缘沿着X = a这条线从上方掉落,
- 因为没有消除,所以我们可以转换成对线段树的操作,比如[a,b]就代表了在[a,a+b-1]这个区间上的高度都增加了b,不能包含a+b,否则会让一侧的掉不下来。
- 因为题目求的是最大的宽度,所以在线段树统计的时候,父节点不再是统计累加和,而是左右子节点的最大值。
- 同时因为这里每次落下来都是增加一个高度,所以我们可以只写一个update方法,每次计算出来高度以后,直接更新到线段树中。
- 还有一个问题就是线段树的范围的问题,因为题目并没有给定具体的x轴的大小,所以我们就要将所有的方块先遍历一遍,然后将其用到的范围离散转化到一个区间内。
- 这个过程我们可以用TreeSet来排序计算出每个方块的左右边界,然后统计出每个点的累计值放到一个map中,这样从map中取到某个坐标的累计值,
- 就是转换成的点的坐标,这样做的好处是将离散的点转成了连续的,节省空间。
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.TreeSet;
/**
* 题目:掉落的方块
* 想象一下标准的俄罗斯方块游戏,X轴是积木最终下落到底的轴线
* 下面是这个游戏的简化版:
* 1)只会下落正方形积木
* 2)[a,b] -> 代表一个边长为b的正方形积木,积木左边缘沿着X = a这条线从上方掉落
* 3)认为整个X轴都可能接住积木,也就是说简化版游戏是没有整体的左右边界的
* 4)没有整体的左右边界,所以简化版游戏不会消除积木,因为不会有哪一层被填满。
* 给定一个N*2的二维数组matrix,可以代表N个积木依次掉落,
* 返回每一次掉落之后的最大高度
* 测试链接:https://leetcode.cn/problems/falling-squares/
*/
public class Q1_FallingSquares {
/**
* 线段树的解法
* 思路:
* 本题目是一个一个的正方形往下落,[a,b] 代表一个边长为b的正方形积木,积木左边缘沿着X = a这条线从上方掉落,
* 因为没有消除,所以我们可以转换成对线段树的操作,比如[a,b]就代表了在[a,a+b-1]这个区间上的高度都增加了b,不能包含a+b,否则会让一侧的掉不下来。
* 因为题目求的是最大的宽度,所以在线段树统计的时候,父节点不再是统计累加和,而是左右子节点的最大值。
* 同时因为这里每次落下来都是增加一个高度,所以我们可以只写一个update方法,每次计算出来高度以后,直接更新到线段树中。
* 还有一个问题就是线段树的范围的问题,因为题目并没有给定具体的x轴的大小,所以我们就要将所有的方块先遍历一遍,然后将其用到的范围离散转化到一个区间内。
* 这个过程我们可以用TreeSet来排序计算出每个方块的左右边界,然后统计出每个点的累计值放到一个map中,这样从map中取到某个坐标的累计值,
* 就是转换成的点的坐标,这样做的好处是将离散的点转成了连续的,节省空间。
*/
public List<Integer> fallingSquares(int[][] positions) {
// 先将所有的坐标连续化,方便线段树的操作
HashMap<Integer, Integer> map = index(positions);
int N = map.size();
SegmentTree segmentTree = new SegmentTree(N);
int max = 0;
List<Integer> res = new ArrayList<>();
// 每落一个正方形,收集一下,所有东西组成的图像,最高高度是什么
for (int[] arr : positions) {
int L = map.get(arr[0]);
int R = map.get(arr[0] + arr[1] - 1);
// 查询当前区间的最大值,计算出新的坐标
int height = segmentTree.query(L, R, 1, N, 1) + arr[1];
max = Math.max(max, height);
res.add(max);
// 更新当前区间的最大值
segmentTree.update(L, R, height, 1, N, 1);
}
return res;
}
/**
* 将离散的坐标转为连续的坐标,方便线段树的操作
*/
public HashMap<Integer, Integer> index(int[][] positions) {
TreeSet<Integer> pos = new TreeSet<>();
for (int[] arr : positions) {
pos.add(arr[0]);
pos.add(arr[0] + arr[1] - 1);
}
HashMap<Integer, Integer> map = new HashMap<>();
int count = 0;
for (Integer index : pos) {
map.put(index, ++count);
}
return map;
}
/**
* 支持最大值的线段树
*/
public static class SegmentTree {
private int[] max;
private int[] change;
private boolean[] update;
public SegmentTree(int size) {
int N = size + 1;
max = new int[N << 2];
change = new int[N << 2];
update = new boolean[N << 2];
}
private void pushUp(int rt) {
max[rt] = Math.max(max[rt << 1], max[rt << 1 | 1]);
}
// ln表示左子树元素结点个数,rn表示右子树结点个数
private void pushDown(int rt, int ln, int rn) {
if (update[rt]) {
update[rt << 1] = true;
update[rt << 1 | 1] = true;
change[rt << 1] = change[rt];
change[rt << 1 | 1] = change[rt];
max[rt << 1] = change[rt];
max[rt << 1 | 1] = change[rt];
update[rt] = false;
}
}
public void update(int L, int R, int C, int l, int r, int rt) {
if (L <= l && r <= R) {
update[rt] = true;
change[rt] = C;
max[rt] = C;
return;
}
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);
if (L <= mid) {
update(L, R, C, l, mid, rt << 1);
}
if (R > mid) {
update(L, R, C, mid + 1, r, rt << 1 | 1);
}
pushUp(rt);
}
public int query(int L, int R, int l, int r, int rt) {
if (L <= l && r <= R) {
return max[rt];
}
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);
int left = 0;
int right = 0;
if (L <= mid) {
left = query(L, R, l, mid, rt << 1);
}
if (R > mid) {
right = query(L, R, mid + 1, r, rt << 1 | 1);
}
return Math.max(left, right);
}
}
}
后记
个人学习总结笔记,不能保证非常详细,轻喷