【12 月小记】Part 2: 线段树 |【1 月小记】Part 1: 扫描线

线段树

一、用法

1. 树的节点

注意:线段树的节点数要开到数组的四倍大小

一般来说,节点中要存储:

  • 懒标记,使用 Lzy 结构体
  • 节点信息,使用 Info 结构体

如果节点信息特别少,可以不用这两个结构体存储。

例(线段树 9):

struct Lzy {
	int add;
	friend Lzy operator + (Lzy a, Lzy b) {
		return {a.add + b.add};
	}
};
struct Info {
	int minv, cnt;
	friend Info operator + (Info a, Info b) {
		if (a.minv == b.minv) return {a.minv, a.cnt + b.cnt};
		if (a.minv < b.minv) return a;
		else return b;
	}
	friend Info operator + (Info a, Lzy b) {
		return {a.minv + b.add, a.cnt};
	}
	void print() {
		cout << minv << ' ' << cnt << '\n';
	}
};
struct Node {
	Info info;
	Lzy lzy;
} t[N << 2];

2. 宏

线段树的每个节点都有一个编号。因为它是一棵二叉树,所以节点编号为 id 的左儿子和右儿子分别是 id << 1id << 1 | 1

这样写字数太多了,可以搞一个宏定义。同时,我们也可以把 mid 搞上宏定义。

记得加上括号。

#define mid (l + r >> 1)
#define lson (id << 1)
#define rson (id << 1 | 1)

3. 节点更新 (update / pushup)

更新每一个结点的信息。这在每道题中的写法不同。

例(线段树 1):

void pushup(int id) {
	t[id].sum = t[lson].sum + t[rson].sum;
	t[id].siz = t[lson].siz + t[rson].siz;
}

4. 懒标记放置 (setlzy)

需要将节点的 Info 和 Lzy 都打上懒标记。

例(线段树 1):

void setlzy(int id, int lzy, int siz) {
	t[id].lzy += lzy;
	t[id].sum += lzy * siz;
}

5. 懒标记下传 (pushdown)

需要判断某一节点的懒标记不为空,然后再把该节点的懒标记放置到其左右节点(使用 setlzy 方法),再把该节点的懒标记置空。

例(线段树 1):

void pushdown(int id) {
	if (t[id].lzy != 0) {
		setlzy(lson, t[id].lzy);
		setlzy(rson, t[id].lzy);
		t[id].lzy = 0;
	}
}

6. 建树 (build)

可以选择在建树时为每个节点的 siz 属性(即节点所代表线段的长度)赋上初始值 r-l+1(同样,也可以选择 pushup 时赋值)。

不排除存在其他信息,也要在建树时初始化的可能性。

递归地访问整个数组,将这个数组不断劈成两半,直到不能再劈为止。此时,节点的 val 就是目前指向的数组值。

最后需要将两个线段合并到它们的父亲,所以需要 pushup 一下。

例(线段树 1):

void build(int id, int l, int r) {
	t[id].siz = r - l + 1;
	if (l == r) {
		t[id].sum = a[l];
	} else {
		build(lson, l, mid);
		build(rson, mid + 1, r);
		pushup(id);
	}
}

修改和查询操作我觉得没什么好说的,很好理解

7. 修改

(1) 单点修改 (change)

例(单点修改+区间最大值):

void change(int id, int l, int r, int pos, int val) {
	if (l == r) t[id].maxv = val;
	else {
		if (pos <= mid) change(lson, l, mid, pos, val);
		else change(rson, mid + 1, r, pos, val);
		update(id);
	}
}

(2) 区间修改 (modify)

例(线段树 1):

void modify(int id, int l, int r, int ql, int qr, int lzy) {
	if (ql == l && qr == r) {
		setlzy(id, lzy);
		return;
	} else {
		pushdown(id);
		if (qr <= mid) modify(lson, l, mid, ql, qr, lzy);
		else if (ql > mid) modify(rson, mid + 1, r, ql, qr, lzy);
		else {
			modify(lson, l, mid, ql, mid, lzy);
			modify(rson, mid + 1, r, mid + 1, qr, lzy);
		}
		pushup(id);
	}
}

8. 区间查询 (query)

例(线段树 1):

int query(int id, int l, int r, int ql, int qr) {
	if (ql == l && qr == r) {
		return t[id].sum;
	} else {
		pushdown(id);
		if (qr <= mid) return query(lson, l, mid, ql, qr);
		else if (ql > mid) return query(rson, mid + 1, r, ql, qr);
		else return query(lson, l, mid, ql, mid) + query(rson, mid + 1, r, mid + 1, qr);
	}
}

二、例题

1. 不带懒标记的线段树

(1) U537775 单点修改+区间最大值

(2) T562495 线段树 - 最小值的出现次数

(3) T563191 线段树 - 最大子段和

这个线段树让你维护的是 MSS(最大子段和),所以我们要在节点里维护一些信息,以使得 MSS 能被计算出来。

考虑维护最大前缀(mpre),最大后缀(msuf),区间和(sum)。

我们可以将区间 [l, r] 分治成 [l, mid] 和 [mid + 1, r],如此,mss 的计算便分为以下三种情况:

  1. 只选左边的区间,mss = left.mss
  2. 只选右边的区间,mss = right.mss
  3. 两边的区间都选,相当于左边的最大后缀以及右边的最大前缀拼合起来生成整个区间的 mss,即 mss = left.msuf + right.mpre

以上三种情况取 max。

至于 mpre、msuf 和 sum 的 计算方法,这很简单了,还是刚才的分治思想。

2. 带懒标记的线段树

(1) T583486 线段树 9

(2) P3372 【模板】线段树 1

(3) P3373 【模板】线段树 2

(4) P1253 扶苏的问题

(5) P6492 [COCI 2010/2011 #6] STEP

很板子,也非常有代表性的一题,类似 T583486 线段树 9 这种问题,都利用了分治思想来维护一些信息。

题意:维护一个 0-1 序列,支持以下两种操作:1)将某一位置的数取反;2)统计整个数列里最长的 0-1 交替排列的子串长度(以下称之为合法串)。

我们考虑使用分治。一个序列的合法串的来源,总有以下三处:

  1. 仅来源于左区间,当且仅当左右区间无法合并;

  2. 仅来源于右区间,当且仅当左右区间无法合并;

  3. 左区间的最长合法后缀 + 右区间的最长合法前缀,当且仅当左右区间能够合并。

这就是我们维护合法区间长度的方法。那么怎么维护前后缀呢?也非常简单。事实上,一个区间的合法前缀来源于:

  1. 左区间的合法前缀,当且仅当左区间的合法前缀长度小于其区间长度;

  2. 左区间 + 右区间的合法前缀,当且仅当左区间的合法前缀长度等于其区间长度,且左右区间能够合并。

维护合法后缀的方法同理。

从上面的方案,可以看出,我们需要维护以下内容:

  1. 某区间内最长合法串的长度(即答案);

  2. 某区间的最长合法前后缀;

  3. 某区间的左右端点值(便于判断两区间能否合并;若左区间的右端点与右区间的左端点不同,则说明这两个区间能合并,反之则不能);

  4. 某区间的长度(维护前后缀长度时,便于判断能否将左右区间拼接)。

我们当然要使用线段树,但是这道题由于是单点修改,所以我们不用写懒标记;而且,因为每次查询的都是全局的最长合法序列长度,所以 query 操作也不用写,直接返回 t[1].info.mseq 即可。

(6) P1558 色板游戏

状压思想 + 线段树维护。

(7) P2572 [SCOI2010] 序列操作

这道题可谓是使用线段树维护 0-1 序列操作模板的集大成者,综合了多种思想,但最终还是模板。

我们的操作有两种:1)置;2)取反。所以我们要搞出来两个懒标记。下面考虑懒标记合并。

我们将一个新的懒标记合并到当前的懒标记中。如果新的懒标记中存在置操作,那么当前的懒标记的置,就等于新的懒标记的置;又由于当前区间已经被置了,所以取反操作也没什么用了,当前的懒标记的取反就等于 0。如果新的懒标记中存在取反操作,那么如果当前的懒标记中存在置操作,则将置操作取反;否则将取反操作取反。

由于我们要维护 1 的个数和连续的 1 的个数,所以我们需要在线段树的节点上维护:1)最长前缀或后缀;2)1 的个数;3)大小;4)最长连续 1 子段的长度。

又因为存在取反操作,所以这种操作可能会使得 1 的信息和 0 的信息正好颠倒,进而我们需要分别维护关于 0 和 1 的上述信息。

然后利用最大子段和的思想分治,其实这题就做差不多了,代码量非常非常大,需要仔细调。

3. 线段树上二分

(1) U502676 线段树上二分模版

扫描线

posted @ 2026-01-05 12:59  L-Coding  阅读(0)  评论(0)    收藏  举报