深入理解线段树
来源 | OSCHINA 社区
作者 | 京东云开发者-王奕龙
原文链接:https://my.oschina.net/u/4090830/blog/10116569
RMQ (Range Minimum/Maximum Query) 问题是指:对于长度为 n 的数列 A,回答若干询问 RMQ (A, i, j) 其中 i, j <= n,返回数列 A 中下标在 i, j 里的最小 (大)值。也就是说:RMQ 问题是指求区间最值的问题。通常该类型题目的解法有递归分治、动态规划、线段树和单调栈 / 单调队列。
1. 线段树
区间查询和单点修改线段树
/**
* 定义线段树节点
*/
class Node {
/**
* 区间和 或 区间最大/最小值
*/
int val;
int left;
int right;
public Node(int left, int right) {
this.left = left;
this.right = right;
}
}
注意其中的 val 字段保存的是区间的和。定义完树的节点,我们来看一下建树的逻辑,注意代码中的注释,我们为线段树分配的节点数组大小为原数组大小的 4 倍,这是考虑到数组转换成满二叉树的最坏情况。
public SegmentTree(int[] nums) {
this.nums = nums;
tree = new Node[nums.length * 4];
// 建树,注意表示区间时使用的是从 1 开始的索引值
build(1, 1, nums.length);
}
/**
* 建树
*
* @param pos 当前节点编号
* @param left 当前节点区间下界
* @param right 当前节点区间上界
*/
private void build(int pos, int left, int right) {
// 创建节点
tree[pos] = new Node(left, right);
// 递归结束条件
if (left == right) {
// 赋值
tree[pos].val = nums[left - 1];
return;
}
// 如果没有到根节点,则继续递归
int mid = left + right >> 1;
build(pos << 1, left, mid);
build(pos << 1 | 1, mid + 1, right);
// 当前节点的值是左子树和右子树节点的和
pushUp(pos);
}
/**
* 用于向上回溯时修改父节点的值
*/
private void pushUp(int pos) {
tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
}
build()
方法执行时,我们会先在对应的位置上创建节点而不进行赋值,只有在递归到叶子节点时才赋值,此时区间大小为 1,节点值即为当前区间的值。之后非叶子节点值都是通过 pushUp()
方法回溯加和当前节点的两个子节点值得出来的。 /**
* 修改单节点的值
*
* @param pos 当前节点编号
* @param numPos 需要修改的区间中值的位置
* @param val 修改后的值
*/
private void update(int pos, int numPos, int val) {
// 找到该数值所在线段树中的叶子节点
if (tree[pos].left == numPos && tree[pos].right == numPos) {
tree[pos].val = val;
return;
}
// 如果不是当前节点那么需要判断是去左或右去找
int mid = tree[pos].left + tree[pos].right >> 1;
if (numPos <= mid) {
update(pos << 1, numPos, val);
} else {
update(pos << 1 | 1, numPos, val);
}
// 叶子节点的值修改完了,需要回溯更新所有相关父节点的值
pushUp(pos);
}
pushUp()
方法对所有相关父节点值进行更新。 /**
* 查找对应区间的值
*
* @param pos 当前节点
* @param left 要查询的区间的下界
* @param right 要查询的区间的上界
*/
private int query(int pos, int left, int right) {
// 如果我们要查找的区间把当前节点区间全部包含起来
if (left <= tree[pos].left && tree[pos].right <= right) {
return tree[pos].val;
}
int res = 0;
int mid = tree[pos].left + tree[pos].right >> 1;
// 根据区间范围去左右节点分别查找求和
if (left <= mid) {
res += query(pos << 1, left, right);
}
if (right > mid) {
res += query(pos << 1 | 1, left, right);
}
return res;
}
public class SegmentTree {
/**
* 定义线段树节点
*/
static class Node {
/**
* 区间和 或 区间最大/最小值
*/
int val;
int left;
int right;
public Node(int left, int right) {
this.left = left;
this.right = right;
}
}
Node[] tree;
int[] nums;
public SegmentTree(int[] nums) {
this.nums = nums;
tree = new Node[nums.length * 4];
// 建树,注意表示区间时使用的是从 1 开始的索引值
build(1, 1, nums.length);
}
/**
* 建树
*
* @param pos 当前节点编号
* @param left 当前节点区间下界
* @param right 当前节点区间上界
*/
private void build(int pos, int left, int right) {
// 创建节点
tree[pos] = new Node(left, right);
// 递归结束条件
if (left == right) {
// 赋值
tree[pos].val = nums[left - 1];
return;
}
// 如果没有到根节点,则继续递归
int mid = left + right >> 1;
build(pos << 1, left, mid);
build(pos << 1 | 1, mid + 1, right);
// 当前节点的值是左子树和右子树节点的和
pushUp(pos);
}
/**
* 修改单节点的值
*
* @param pos 当前节点编号
* @param numPos 需要修改的区间中值的位置
* @param val 修改后的值
*/
private void update(int pos, int numPos, int val) {
// 找到该数值所在线段树种的叶子节点
if (tree[pos].left == numPos && tree[pos].right == numPos) {
tree[pos].val = val;
return;
}
// 如果不是当前节点那么需要判断是去左或右去找
int mid = tree[pos].left + tree[pos].right >> 1;
if (numPos <= mid) {
update(pos << 1, numPos, val);
} else {
update(pos << 1 | 1, numPos, val);
}
// 叶子节点的值修改完了,需要回溯更新所有相关父节点的值
pushUp(pos);
}
/**
* 用于向上回溯时修改父节点的值
*/
private void pushUp(int pos) {
tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
}
/**
* 查找对应区间的值
*
* @param pos 当前节点
* @param left 要查询的区间的下界
* @param right 要查询的区间的上界
*/
private int query(int pos, int left, int right) {
// 如果我们要查找的区间把当前节点区间全部包含起来
if (left <= tree[pos].left && tree[pos].right <= right) {
return tree[pos].val;
}
int res = 0;
int mid = tree[pos].left + tree[pos].right >> 1;
// 根据区间范围去左右节点分别查找求和
if (left <= mid) {
res += query(pos << 1, left, right);
}
if (right > mid) {
res += query(pos << 1 | 1, left, right);
}
return res;
}
}
2. 线段树的区间修改与懒惰标记
只要不访问到当前区间的子区间,那么子区间值始终都不会变化,相当于子区间值的变化量被当前节点通过 add 字段 “持有”
static class Node {
int left;
int right;
int val;
int add;
public Node(int left, int right) {
this.left = left;
this.right = right;
}
}
区间修改
/**
* 修改区间的值
*
* @param pos 当前节点编号
* @param left 要修改区间的下界
* @param right 要修改区间的上界
* @param val 区间内每个值的变化量
*/
public void update(int pos, int left, int right, int val) {
// 如果该区间被要修改的区间包围的话,那么需要将该区间所有的值都修改
if (left <= tree[pos].left && tree[pos].right <= right) {
tree[pos].val += (tree[pos].right - tree[pos].left + 1) * val;
// 懒惰标记
tree[pos].add += val;
return;
}
// 该区间没有被包围的话,需要修改节点的信息
pushDown(pos);
int mid = tree[pos].left + tree[pos].right >> 1;
// 如果下界在 mid 左边,那么左子树需要修改
if (left <= mid) {
update(pos << 1, left, right, val);
}
// 如果上界在 mid 右边,那么右子树也需要修改
if (right > mid) {
update(pos << 1 | 1, left, right, val);
}
// 修改完成后向上回溯修改父节点的值
pushUp(pos);
}
private void pushDown(int pos) {
// 根节点 和 懒惰标记为 0 的情况不需要再向下遍历
if (tree[pos].left != tree[pos].right && tree[pos].add != 0) {
int add = tree[pos].add;
// 计算累加变化量
tree[pos << 1].val += add * (tree[pos << 1].right - tree[pos << 1].left + 1);
tree[pos << 1 | 1].val += add * (tree[pos << 1 | 1].right - tree[pos << 1 | 1].left + 1);
// 子节点懒惰标记累加
tree[pos << 1].add += add;
tree[pos << 1 | 1].add += add;
// 懒惰标记清 0
tree[pos].add = 0;
}
}
private void pushUp(int pos) {
tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
}
区间查询
public int query(int pos, int left, int right) {
if (left <= tree[pos].left && tree[pos].right <= right) {
// 当前区间被包围
return tree[pos].val;
}
// 懒惰标记需要下传修改子节点的值
pushDown(pos);
int res = 0;
int mid = tree[pos].left + tree[pos].right >> 1;
if (left <= mid) {
res += query(pos << 1, left, right);
}
if (right > mid) {
res += query(pos << 1 | 1, left, right);
}
return res;
}
public class SegmentTree2 {
static class Node {
int left;
int right;
int val;
int add;
public Node(int left, int right) {
this.left = left;
this.right = right;
}
}
Node[] tree;
int[] nums;
public SegmentTree2(int[] nums) {
this.tree = new Node[nums.length * 4];
this.nums = nums;
build(1, 1, nums.length);
}
private void build(int pos, int left, int right) {
tree[pos] = new Node(left, right);
// 递归结束条件
if (left == right) {
tree[pos].val = nums[left - 1];
return;
}
int mid = left + right >> 1;
build(pos << 1, left, mid);
build(pos << 1 | 1, mid + 1, right);
// 回溯修改父节点的值
pushUp(pos);
}
/**
* 修改区间的值
*
* @param pos 当前节点编号
* @param left 要修改区间的下界
* @param right 要修改区间的上界
* @param val 区间内每个值的变化量
*/
public void update(int pos, int left, int right, int val) {
// 如果该区间被要修改的区间包围的话,那么需要将该区间所有的值都修改
if (left <= tree[pos].left && tree[pos].right <= right) {
tree[pos].val += (tree[pos].right - tree[pos].left + 1) * val;
// 懒惰标记
tree[pos].add += val;
return;
}
// 该区间没有被包围的话,需要修改节点的信息
pushDown(pos);
int mid = tree[pos].left + tree[pos].right >> 1;
// 如果下界在 mid 左边,那么左子树需要修改
if (left <= mid) {
update(pos << 1, left, right, val);
}
// 如果上界在 mid 右边,那么右子树也需要修改
if (right > mid) {
update(pos << 1 | 1, left, right, val);
}
// 修改完成后向上回溯修改父节点的值
pushUp(pos);
}
public int query(int pos, int left, int right) {
if (left <= tree[pos].left && tree[pos].right <= right) {
// 当前区间被包围
return tree[pos].val;
}
// 懒惰标记需要下传修改子节点的值
pushDown(pos);
int res = 0;
int mid = tree[pos].left + tree[pos].right >> 1;
if (left <= mid) {
res += query(pos << 1, left, right);
}
if (right > mid) {
res += query(pos << 1 | 1, left, right);
}
return res;
}
private void pushDown(int pos) {
// 根节点 和 懒惰标记为 0 的情况不需要再向下遍历
if (tree[pos].left != tree[pos].right && tree[pos].add != 0) {
int add = tree[pos].add;
// 计算累加变化量
tree[pos << 1].val += add * (tree[pos << 1].right - tree[pos << 1].left + 1);
tree[pos << 1 | 1].val += add * (tree[pos << 1 | 1].right - tree[pos << 1 | 1].left + 1);
// 子节点懒惰标记
tree[pos << 1].add += add;
tree[pos << 1 | 1].add += add;
// 懒惰标记清 0
tree[pos].add = 0;
}
}
private void pushUp(int pos) {
tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
}
}
3. 线段树动态开点
pos << 1
和 pos << 1 | 1
来寻找当前节点的左右子节点,取而代之的是在节点中使用 left 和 right 记录左右子节点在 tree [] 中的位置,这一点需要注意: static class Node {
// left 和 right 不再表示区间范围而是表示左右子节点在 tree 中的索引位置
int left, right;
int val;
int add;
}
/**
* 修改区间的值
*
* @param pos 当前节点的索引值
* @param left 当前线段树节点表示的范围下界
* @param right 当前线段树节点表示的范围上界
* @param l 要修改的区间下界
* @param r 要修改的区间上界
* @param val 区间值变化的大小
*/
public void update(int pos, int left, int right, int l, int r, int val) {
// 当前区间被要修改的区间全部包含
if (l <= left && right <= r) {
tree[pos].val += (right - left + 1) * val;
tree[pos].add += val;
return;
}
lazyCreate(pos);
pushDown(pos, right - left + 1);
int mid = left + right >> 1;
if (l <= mid) {
update(tree[pos].left, left, mid, l, r, val);
}
if (r > mid) {
update(tree[pos].right, mid + 1, right, l, r, val);
}
pushUp(pos);
}
// 为该位置创建节点
private void lazyCreate(int pos) {
if (tree[pos] == null) {
tree[pos] = new Node();
}
// 创建左子树节点
if (tree[pos].left == 0) {
tree[pos].left = ++count;
tree[tree[pos].left] = new Node();
}
// 创建右子树节点
if (tree[pos].right == 0) {
tree[pos].right = ++count;
tree[tree[pos].right] = new Node();
}
}
private void pushDown(int pos, int len) {
if (tree[pos].left != 0 && tree[pos].right != 0 && tree[pos].add != 0) {
// 计算左右子树的值
tree[tree[pos].left].val += (len - len / 2) * tree[pos].add;
tree[tree[pos].right].val += len / 2 * tree[pos].add;
// 子节点懒惰标记
tree[tree[pos].left].add += tree[pos].add;
tree[tree[pos].right].add += tree[pos].add;
tree[pos].add = 0;
}
}
private void pushUp(int pos) {
tree[pos].val = tree[tree[pos].left].val + tree[tree[pos].right].val;
}
public class SegmentTree3 {
static class Node {
// left 和 right 不再表示区间范围而是表示左右子节点在 tree 中的索引位置
int left, right;
int val;
int add;
}
// 记录当前节点数
int count;
Node[] tree;
public SegmentTree3() {
count = 1;
tree = new Node[(int) 5e6];
tree[count] = new Node();
}
public int query(int pos, int left, int right, int l, int r) {
if (l <= left && right <= r) {
return tree[pos].val;
}
lazyCreate(pos);
pushDown(pos, right - left + 1);
int res = 0;
int mid = left + right >> 1;
if (l <= mid) {
res += query(tree[pos].left, left, mid, l, r);
}
if (r > mid) {
res += query(tree[pos].right, mid + 1, right, l, r);
}
return res;
}
/**
* 修改区间的值
*
* @param pos 当前节点的索引值
* @param left 当前线段树节点表示的范围下界
* @param right 当前线段树节点表示的范围上界
* @param l 要修改的区间下界
* @param r 要修改的区间上界
* @param val 区间值变化的大小
*/
public void update(int pos, int left, int right, int l, int r, int val) {
// 当前区间被要修改的区间全部包含
if (l <= left && right <= r) {
tree[pos].val += (right - left + 1) * val;
tree[pos].add += val;
return;
}
lazyCreate(pos);
pushDown(pos, right - left + 1);
int mid = left + right >> 1;
if (l <= mid) {
update(tree[pos].left, left, mid, l, r, val);
}
if (r > mid) {
update(tree[pos].right, mid + 1, right, l, r, val);
}
pushUp(pos);
}
// 为该位置创建节点
private void lazyCreate(int pos) {
if (tree[pos] == null) {
tree[pos] = new Node();
}
// 创建左子树节点
if (tree[pos].left == 0) {
tree[pos].left = ++count;
tree[tree[pos].left] = new Node();
}
// 创建右子树节点
if (tree[pos].right == 0) {
tree[pos].right = ++count;
tree[tree[pos].right] = new Node();
}
}
private void pushDown(int pos, int len) {
if (tree[pos].left != 0 && tree[pos].right != 0 && tree[pos].add != 0) {
// 计算左右子树的值
tree[tree[pos].left].val += (len - len / 2) * tree[pos].add;
tree[tree[pos].right].val += len / 2 * tree[pos].add;
// 子节点懒惰标记
tree[tree[pos].left].add += tree[pos].add;
tree[tree[pos].right].add += tree[pos].add;
tree[pos].add = 0;
}
}
private void pushUp(int pos) {
tree[pos].val = tree[tree[pos].left].val + tree[tree[pos].right].val;
}
}
巨人的肩膀
【宫水三叶】一题三解 :「递归分治」&「线段树」&「单调栈」
线段树浅谈
线段树什么的不是简简单单嘛,我教你!:基础篇
OI-wiki 线段树
维基百科:线段树
【RMQ 专题】关于 RMQ 的若干解法(内含彩蛋)
【宫水三叶】一题双解 :「差分」&「线段树」(附区间求和目录)
【宫水三叶】一题三解 :「模拟」&「线段树(动态开点)」&「分块 + 位运算(分桶)」
END
点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦
微信扫码关注该文公众号作者
戳这里提交新闻线索和高质量文章给我们。
来源: qq
点击查看作者最近其他文章