OI学习笔记4:线段树
线段树
一、线段树
1、线段树:
- 线段树是算法竞赛中常用来维护区间信息的数据结构。
- 线段树可以在 \(O(\log N)\) 的时间复杂度内实现:
单点修改,区间修改,区间查询(求和,最大值,最小值)等操作。
2、数学上理解线段树。
- 线段树维护的信息在很多时候可以认为是满足幺(半)群性质的信息。
- 一个幺(半)群 \(M=(S,\circ, e)\),其中 \(\circ\) 为在集合 \(S\) 上定义的二元运算符, \(M\) 具有以下性质:
- 封闭性:\(\forall x \in S\) 和 \(\forall y \in S\),有 \(x \circ y \in S\)。
- 结合律:\(\forall x,y,z \in S\),有 \((x \circ y)\circ z=x \circ (y\circ z)\)。
- 存在幺元:\(\exists e \in S\),满足 \(\forall x \in S\),有 \(e\circ x=x\),\(e\) 为左幺元;或 \(x\circ e=x\),\(e\) 为右幺元。
二、基本结构与建树
- 线段树将每个长度不为 \(1\) 的区间划分为左右两个区间递归建树。
void build(int p, int l, int r){
if(l == r){
d[p] = a[l];
return;
}
int mid = (l + r) >> 1;
build(p<<1, l, mid);
build(p<<1|1, mid + 1, r);
d[p] = d[p<<1] + d[p<<1|1];
}
三、区间修改与懒标记
1、懒标记的作用。
- 懒标记保存父节点对子节点的待定操作,等到查询到该节点时再将操作下放。
2、两种不同的懒标记应用。
(1)加上、减去某个值。
void update(int nl, int nr, int l, int r, int c, int p){
if(nl <= l && r <= nr){
d[p] += (r - l + 1) * c, tag[p] += c;
return;
}
int mid = (l + r) >> 1;
if(tag[p] && l != r){//懒标记下放
d[p << 1] += tag[p] * (mid - l + 1);
d[p << 1 | 1] += tag[p] * (r - mid);
tag[p<<1] += tag[p], tag[p<<1|1] += tag[p];
tag[p] = 0;
}
if(nl <= mid) update(nl, nr, l, mid, c, p << 1);
if(nr > mid) update(nl, nr, mid + 1, r, c, p << 1 | 1);
d[p] = d[p<<1] + d[p<<1|1];
}
(2)修改某个值。
void update(int nl, int nr, int l, int r, int c, int p){
if(nl <= l && r <= nr){
d[p] = (r - l + 1) * c, tag[p] = c;
return;
}
int mid = (l + r) >> 1;
if(tag[p]){//懒标记下放
d[p << 1] = tag[p] * (mid - l + 1);
d[p << 1 | 1] = tag[p] * (r - mid);
tag[p<<1] = tag[p], tag[p<<1|1] = tag[p];
tag[p] = 0;
}
if(nl <= mid) update(nl, nr, l, mid, c, p << 1);
if(nr > mid) update(nl, nr, mid + 1, r, c, p << 1 | 1);
d[p] = d[p<<1] + d[p<<1|1];
}
四、区间查询
1、求和。
int query(int qx, int qy, int l, int r, int p){
if(qx <= l && r <= qy) return d[p];
int mid = (l + r) >> 1;
if(tag[p]){
d[p<<1]=tag[p] * (mid - l + 1);
d[p<<1|1]=tag[p] * (r - mid);
tag[p<<1] = tag[p<<1|1] = tag[p];
tag[p] = 0;
}
int sum = 0;
if(qx <= mid) sum += query(qx, qy, l, mid, p<<1);
if(qy > mid) sum += query(qx, qy, mid + 1, r, p<<1|1);
return sum;
}
2、最值。
(1)最大值。
int query(int qx, int qy, int l, int r, int p){
if(qx <= l && r <= qy) return d[p];
int mid = (l + r) >> 1;
int ans = 0;
if(qx <= mid) ans = max(ans, query(qx, qy, l, mid, p<<1));
if(qy > mid) ans = max(ans, query(qx, qy, mid + 1, r, p<<1|1));
return ans;
}
(2)最小值。
int query(int qx, int qy, int l, int r, int p){
if(qx <= l && r <= qy) return d[p];
int mid = (l + r) >> 1;
int ans = 0;
if(qx <= mid) ans = min(ans, query(qx, qy, l, mid, p<<1));
if(qy > mid) ans = min(ans, query(qx, qy, mid + 1, r, p<<1|1));
return ans;
}
五、优化策略
- 叶子节点处无需下放懒标记。
- 下放懒标记可写一个专门的 \(pushdown\) 函数,从儿子节点更新父亲节点也可以写一个专门的函数 \(pushup\),降低编写难度。
- 标记永久化。如果确定懒标记不会被加到溢出,就可以将标记永久化。它可以避免下传懒标记,只需在询问时把标记的影响加到答案中去,从而降低程序常数。
六、注意事项
-
不要有冗余代码与变量。
-
不要在修改算法正确性的情况下胡乱增删代码只为过样例。
-
query操作一般要pushdown,特殊情况不用。
-
注意题目特殊条件。
-
二维线段树只用在第二维建树, 第一维存第二维线段树的根结点。
-
二维线段树函数使用时注意各个参量的确切含义,读题时注意操作的类型,以便确定各类操作的区间范围。
-
二维线段树一般不用懒标记。
-
update函数中的判断条件是
(lx <= l && r<= rx)
或者(x <= d[p].left && d[p].right <= y)
,前者无结构体,后者有结构体,有时还需判断交集和结点的染色情况。 -
query函数中的判断条件是
(l == r)
,有时与update函数的第一个判断条件相同(维护加乘减法操作时是这样)。 -
一维线段树维护加法与乘法,懒标记下放时遵循“先乘后加”原则。
-
线段树的结构体写法中,区间包括在结构体中,函数内判定时直接使用当前结点的区间,适合写覆盖操作,写覆盖操作时父结点要清零。
-
线段树的数组写法中,区间要额外引入,懒标记下传方便,适合写四则运算操作。
-
覆盖操作注意是左闭右开区间,要减1。