线段树概述:
- 每个节点表示一段区间
- 具有唯一根节点,表示整个统计范围
- 每个叶节点代表一个长度为1的元区间
基本操作
pushup(int u)
由子节点算父节点信息,例如sum = l.sum + r.sum
build()
将一段区间初始化为线段树
用一维数组存储整棵树,类似于堆。
编号为x
的节点
- 父节点 `x / 2`或 `x >> 1`
- 左儿子 `2x` 或 `x << 1`
- 右儿子 `2x` 或 `x << 1 | 1`
modify()
- 修改一个点
- 修改一段区间
pushdown()
,即懒标记/延迟标记。
将父节点的信息更新到子节点
query()
查询某一段区间的信息pushdown()
操作
灵感来自于query
操作,新增add
字段作为懒标记
给以当前节点为根的子树中的每一个节点加上add
(不包含根节点)
相应的,使用懒标记后查询操作也得发生变化,先pushdown
再query
,修改操作先pushdown
再modify
一个疑问:为什么modify的时候也需要先pushdown
? 如果被打上懒标记的区间的一个子区间被修改,势必会执行pushup
操作,而根区间由于懒标记并未向下传递会导致子区间在原来的数值上修改并影响了跟区间,从而导致懒标记毫无作用。
//初始化
N = <输入规模>
Node[] tr = new Node[4 * N];
build(int u, int l, int r) {
tr[u] = new Node(l, r);
if (l == r) {
对tr[u]的其它属性进行赋值操作
}
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid, r);
pushup(u);
}
//pushup
这个操作时间复杂度一般为O(1)
应用在递归回退的过程中用子节点更新父节点
//查询
/*
待查询区间为[l, r], 当前线段树节点区间为[Tl, Tr]
①:[Tl, Tr] ∈[l, r] 直接返回
②:[Tl, Tr] ∩ [l, r] != 空 //一定成立,因为不会遍历那些没有交集的节点
时间复杂度约为4lgn,n指待查询区间(r - l + 1)
*/
//单点修改
只需要从上往下递归遍历某一分支,时间复杂度为O(lgn)
原理
整棵线段树是一棵二叉树,并且除了最后一层,是一个满二叉树。
树的深度为O(logN)
,可以用类似堆的方法,用数组存储线段树。
分界点取法:mid = l + r >> 1
左半: [l, mid]
右半: [mid + 1, r]
编号为x
,父节点为 x >> 1
,左儿子为 x << 1
,右儿子为 x << 1 | 1
存储空间开多大?4 * n
4N 即全树节点数估计,区间长度为N的线段树,理想情况下N个叶节点的满二叉树有N + N / 2 + N / 4 + ... + 1 = 2N - 1
个节点。使用数组存储时最后一层会有空余,所以保存线段树的数组长度要不小于4N
保证不会越界。
问题类型
- 单点修改,区间查询
- 维护懒标记,区间修改,区间查询
AcWing 243. 一个简单的整数问题2
AcWing 1277. 维护序列
- 扫描线
- 动态开点线段树
699. 掉落的方块
注意本题有点坑:某段区间变高是在添加之前的最高一段区间的基础上加的
方法1:面向对象动态开点
class Solution {
public List<Integer> fallingSquares(int[][] positions) {
Node root = new Node(1, (int)(1e9));
List<Integer> res = new ArrayList<>();
for (int[] p : positions) {
int h = root.query(p[0], p[0] + p[1] - 1);
root.modify(p[0], p[0] + p[1] - 1, p[1] + h);
res.add(root.height);
}
return res;
}
}
class Node {
int l, r;
int height, add;
Node left, right;
Node(int l, int r) {
this.l = l;
this.r = r;
}
int query(int l, int r) {
if (l <= this.l && r >= this.r)
return height;
else {
int mid = this.l + this.r >> 1;
if (this.left == null) {
this.left = new Node(this.l, mid);
}
if (this.right == null) {
this.right = new Node(mid + 1, this.r);
}
pushdown();
int res = 0;
if (l <= mid) res = left.query(l, r);
if (r > mid) res = Math.max(res, right.query(l, r));
return res;
}
}
void modify(int l, int r, int x) {
if (l <= this.l && r >= this.r) {
this.height = x;
this.add = x;
} else {
int mid = this.l + this.r >> 1;
if (this.left == null) {
this.left = new Node(this.l, mid);
}
if (this.right == null) {
this.right = new Node(mid + 1, this.r);
}
pushdown();
if (l <= mid) this.left.modify(l, r, x);
if (r > mid) this.right.modify(l, r, x);
pushup();
}
}
void pushup() {
height = Math.max(right.height, left.height);
}
void pushdown() {
if (add > 0) {
left.height = add;
left.add = add;
right.height = add;
right.add = add;
add = 0;
}
}
}
方法2:面向过程动态开点
预估节点数为查询次数 6 log(n),n为总区间大小
class Solution {
class Node {
int idxl, idxr;
int height;
int add;
}
Node[] tr = new Node[240010];
int idx = 1;
public List<Integer> fallingSquares(int[][] positions) {
List<Integer> res = new ArrayList<>();
for (int[] p : positions) {
int h = query(1, 1, (int)(1e9), p[0], p[1] + p[0] - 1);
modify(1, 1, (int)(1e9), p[0], p[0] + p[1] - 1, p[1] + h);
res.add(tr[1].height);
}
return res;
}
void modify(int u, int l, int r, int ql, int qr, int x) {
if (ql <= l && qr >= r) {
tr[u].height = x;
tr[u].add = x;
} else {
int mid = l + r >> 1;
creatNode(u);
pushdown(u);
if (ql <= mid) modify(tr[u].idxl, l, mid, ql, qr, x);
if (qr > mid) modify(tr[u].idxr, mid + 1, r, ql, qr, x);
pushup(u);
}
}
int query(int u, int l, int r, int ql, int qr) {
if (ql <= l && qr >= r) {
return tr[u].height;
} else {
int mid = l + r >> 1;
creatNode(u);
pushdown(u);
int res = 0;
if (ql <= mid) res = Math.max(res, query(tr[u].idxl, l, mid, ql, qr));
if (qr > mid) res = Math.max(res, query(tr[u].idxr, mid + 1, r, ql, qr));
return res;
}
}
void pushup(int u) {
tr[u].height = Math.max(tr[tr[u].idxl].height, tr[tr[u].idxr].height);
}
void pushdown(int u) {
if (tr[u].add > 0) {
Node left = tr[tr[u].idxl], right = tr[tr[u].idxr];
left.height = tr[u].add;
left.add = tr[u].add;
right.height = tr[u].add;
right.add = tr[u].add;
tr[u].add = 0;
}
}
void creatNode(int u) {
if (tr[u] == null)
tr[u] = new Node();
if (tr[u].idxl == 0) {
tr[u].idxl = ++idx;
tr[idx] = new Node();
}
if (tr[u].idxr == 0) {
tr[u].idxr = ++idx;
tr[idx] = new Node();
}
}
}
- 挖个坑,非递归线段树
Efficient and easy segment trees
算法学习笔记(76): zkw线段树