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。

posted @ 2021-08-27 16:04  聂玄HankNie  阅读(138)  评论(0编辑  收藏  举报