线段树

线段树

线段树是算法竞赛中最强有力的数据结构之一,本文将介绍多种维护线段树的技巧。

简介

线段树用途广泛且灵活,其核心思想在于分治,将原区间折半分治为两个大小相同的子区间,分别建树维护。

同时,线段树借助懒惰标记的思想,使得分治的递归可以不必分治到叶子节点,而是将操作的区间拆分到了 \(O(\log n)\) 个节点上,从而实现了优秀的对数级时间复杂度。

作为树形结构,线段树也可以实现高效的合并 / 可持久化等操作。

由于线段树的核心操作只有合并,因此线段树可以维护任意半群信息。也就是说,只要满足封闭性和结合律,线段树就可以维护。

一些 \(\text{tricks}\)

  • 如果线段树所需维护的信息过多,可以单独定义一个结构体维护半群的信息,并对结构体重载运算符,从而封装代码,简化调试。
    这种方法还有一个好处:使用结构体维护信息的内存地址连续,能更好地利用缓存。
  • 如果变量名过长,可以合理地运用宏定义来简化代码,降低编码难度。

吉司机线段树

吉司机线段树是吉如一\(2016\) 年国家集训队论文《区间最值操作与历史最值问题》提出的一种势能线段树,用来高效地完成区间最值操作与查询历史最值。

区间历史最值:CPU 监控

题意:有 \(n\) 个整数 \(a_i\) 和一个辅助数组 \(b_i\),现有四种操作:

  • Q x y:查询 \(\max_{i = x} ^ y a_i\)
  • A x y:查询 \(\max_{i = x} ^ y b_i\)
  • P x y z\(a_i \leftarrow a_i + z \quad (x \leq i \leq y)\)
  • C x y z\(a_i \leftarrow z \qquad \quad (x \leq i \leq y)\)

在每次操作之后,\(b_i = \max(a_i, b_i)\)

对于区间历史最值的题目,我们可以对每种操作的标记,分别维护一个历史最大标记,记录答案时,同样维护一个历史最大答案。

struct node_t
{
	int max, hax, add, max_add, set, max_set;	// hax is history max
	char is_set, is_add;
	...
};

考虑如何合并信息。

观察到加法 \(\texttt{P}\) 和赋值 \(\texttt{C}\) 这两种操作有一个重要的性质:在进行了一次赋值操作之后,后续的所有操作都可以被视为赋值操作,证明显然。

因此对于一个节点而言,操作 \(\texttt{P P P C C C P C P C P C}\) 可以简化为 \(\texttt{P C}\)


我们首先考虑如何给一个节点赋值并打标记:

  • 对于最大值 max,直接赋值即可。
  • 对于历史最大赋值 max_set 则需要依赖这段操作的历史最大赋值。
    从节点的视角,每次的赋值并打标记的过程并非是只执行了一次赋值的操作,而是执行了从祖先节点积累下来的一段连续的操作(从上次 push_down() 到这次 push_down() 这段时间内的所有祖先上做过的操作)。
    而这一段连续的操作的历史最大赋值并不一定与最终合标记的赋值一样(其实是显然不一样),因此需要单独维护。
  • 对于历史最大值 hax,同样直接用历史最大赋值更新即可。
  • 结束时记得置位 is_set
/**
 * @brief 给节点赋值并打标记
 * 
 * @param x 这一段操作的合操作的赋值
 * @param max_x 这一段操作的最大赋值
 */
inline void Set(int x, int max_x)
{
	chkmax(max_set, max_x), chkmax(hax, max_x);
	max = set = x, is_set = 1;
}

接下来,我们处理加法标记,这与赋值大相径庭。

注意如果节点已经被赋值,则加法标记可以直接转化为赋值标记。

inline void Add(int x, int max_x)
{
	if (is_set) return Set(set + x, set + max_x);
	chkmax(max_add, add + max_x), chkmax(hax, max + max_x);
	max += x, add += x, is_add = 1;
}

其他操作,例如合并两个节点,都比较容易,这里不再过多赘述。

读者在写代码时应当养成封装的好习惯,良好的封装可以提高你的代码复用性,把繁琐的操作维护成一个简单的函数是竞赛时简化代码量和降低调试难度的好方法。

线段树的本体非常好实现,注意下传标记时先传加,再传赋值

最后附上完整的代码:

#include <bits/extc++.h>

#define inline __always_inline
#define lson(u) (son[(u)][0])
#define rson(u) (son[(u)][1])
template <typename T> inline void chkmax(T &x, const T &y) { if (x < y) x = y; }
template <typename T> inline void read(T &x)
{
	char ch; bool flag = false;
	for (ch = getchar(); !isdigit(ch); ch = getchar()) flag = ch == '-';
	for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
	if (flag) x = -x;
}
inline void read(char &ch) { for (ch = getchar(); !isupper(ch); ch = getchar()); }
const int MaxN = 1e5 + 5, MaxSGT = MaxN * 2, neg_inf = 0x80000000;

struct node_t
{
	int max, hax, add, max_add, set, max_set;
	char is_set, is_add;
	inline void clear_set() { is_set = set = 0, max_set = neg_inf; }
	inline void clear_add() { is_add = add = 0, max_add = neg_inf; }
	node_t() { max = hax = 0, clear_set(), clear_add(); }
	inline void Set(int x, int max_x)
	{
		chkmax(max_set, max_x), chkmax(hax, max_x);
		max = set = x, is_set = 1;
	}
	inline void Add(int x, int max_x)
	{
		if (is_set) return Set(set + x, set + max_x);
		chkmax(max_add, add + max_x), chkmax(hax, max + max_x);
		max += x, add += x, is_add = 1;
	}
};
inline node_t operator+(const node_t &x, const node_t &y)
{
	node_t ans;
	ans.max = std::max(x.max, y.max);
	ans.hax = std::max(x.hax, y.hax);
	return ans;
}
struct segment_tree_t
{
	int son[MaxSGT][2], top = 2;
	node_t node[MaxSGT];
	inline void push_up(int u) { node[u] = node[lson(u)] + node[rson(u)]; }
	inline void push_down(int u)
	{
		if (node[u].is_add)
		{
			node[lson(u)].Add(node[u].add, node[u].max_add);
			node[rson(u)].Add(node[u].add, node[u].max_add);
			node[u].clear_add();
		}
		if (node[u].is_set)
		{
			node[lson(u)].Set(node[u].set, node[u].max_set);
			node[rson(u)].Set(node[u].set, node[u].max_set);
			node[u].clear_set();
		}
	}
	void build(int u, int l, int r, int *x)
	{
		if (r - l == 1) { node[u].max = node[u].hax = x[l]; return; };
		build(lson(u) = top++, l, l + r >> 1, x);
		build(rson(u) = top++, l + r >> 1, r, x);
		push_up(u);
	}
	void Set(int u, int l, int r, int pl, int pr, int x)
	{
		if (pl <= l && r <= pr) return node[u].Set(x, x);
		int mid = l + r >> 1; push_down(u);
		if (pl < mid) Set(lson(u), l, mid, pl, pr, x);
		if (mid < pr) Set(rson(u), mid, r, pl, pr, x);
		push_up(u);
	}
	void Add(int u, int l, int r, int pl, int pr, int x)
	{
		if (pl <= l && r <= pr) return node[u].Add(x, x);
		int mid = l + r >> 1; push_down(u);
		if (pl < mid) Add(lson(u), l, mid, pl, pr, x);
		if (mid < pr) Add(rson(u), mid, r, pl, pr, x);
		push_up(u);
	}
	int Max(int u, int l, int r, int pl, int pr)
	{
		if (pl <= l && r <= pr) return node[u].max;
		int mid = l + r >> 1, ans = neg_inf; push_down(u);
		if (pl < mid) chkmax(ans, Max(lson(u), l, mid, pl, pr));
		if (mid < pr) chkmax(ans, Max(rson(u), mid, r, pl, pr));
		return ans;
	}
	int Hax(int u, int l, int r, int pl, int pr)
	{
		if (pl <= l && r <= pr) return node[u].hax;
		int mid = l + r >> 1, ans = neg_inf; push_down(u);
		if (pl < mid) chkmax(ans, Hax(lson(u), l, mid, pl, pr));
		if (mid < pr) chkmax(ans, Hax(rson(u), mid, r, pl, pr));
		return ans;
	}
} tree;

int n, q, l, r, x, w[MaxN]; char op;
int main()
{
	read(n);
	for (int i = 1; i <= n; i++) read(w[i]);
	tree.build(1, 1, n + 1, w);
	read(q);
	for (int i = 0; i < q; i++)
	{
		read(op), read(l), read(r), r++;
		switch (op)
		{
		case 'Q': printf("%d\n", tree.Max(1, 1, n + 1, l, r)); break;
		case 'A': printf("%d\n", tree.Hax(1, 1, n + 1, l, r)); break;
		case 'P': read(x), tree.Add(1, 1, n + 1, l, r, x); break;
		case 'C': read(x), tree.Set(1, 1, n + 1, l, r, x); break;
		default: abort();
		}
	}
	return 0;
}

李超线段树

[SDOI2016] 游戏

\(O(\log n)\) push_up()

楼房重建

本题利用了 \(O(\log n)\) push_up() 这个 \(\text{trick}\)

观察题意,容易想出将楼房的 \(x, y\) 坐标转化为斜率,则原问题被转化为了:

维护一个斜率序列,要求:

  • 单点修改
  • 查询序列的严格上升子序列的长度。

靠前的元素会挡住靠后的元素,也就是说,左区间的数会对右区间的答案产生影响。

因此我们考虑维护节点答案 \(\texttt{cnt}\),其含义为:

  • \(u\) 是叶子节点,则 \(\operatorname{cnt}(u) = 1\)
  • 否则 \(\operatorname{cnt}(u)\) 的含义为 \(u\) 所代表的区间内,无前置限制,仅满足 \(u\) 所管辖的区间内,后元素大于前元素的答案元素个数。

对于每次的查询,答案显然就是根节点的 \(\texttt{cnt}\)\(O(1)\) 输出即可。

对于每次修改,先单点修改掉叶子节点,然后考虑 push_up()

  • 对于最大值的合并是显然的,这里不再赘述。
  • 对于 \(\operatorname{cnt}(u)\),有转移 \(\operatorname{cnt}(u) = \operatorname{cnt}(L(u)) + \operatorname{sum}(R(u), \max(L(u)))\)
    其中,\(\operatorname{sum}(R(u), \max(L(u)))\) 的含义为右区间中比左儿子最大值还大的元素总数,可以继续递归右儿子来求出 \(\operatorname{sum}(R(u), \max(L(u)))\)

考虑如何实现递归求解 \(\operatorname{sum}(u, k)\)

  • \(u\) 是叶子节点,则直接返回 \([\max(u) > k]\) 即可(方括号为艾佛森括号)。
  • \(\max(L(u)) \leq k\),则说明左儿子无法对答案产生任何贡献,直接递归右儿子即可。
  • 否则说明 \(k < \max(L(u))\),则右儿子的所有大于左儿子元素均可以对总答案产生贡献。
    (当然,比左儿子小的节点是永远不可能产生贡献的,我思考答案的充要性卡了很久)
    递归求解左儿子,注意总答案为 \(\operatorname{cnt}(u) - \operatorname{cnt}(L(u)) + \operatorname{sum}(L(u), k)\)

读者也许会有这样的疑问:为什么总答案不能写作简洁的 \(\operatorname{sum}(L(u), k) + \operatorname{cnt}(R(u))\)
其实,只要多思考一下,就会意识到 \(\operatorname{cnt}(R(u))\) 并不能保证右儿子的答案的具体元素比左儿子的最大值要大,因此正确的做法是用总答案替换掉左儿子部分的答案。

分析时间复杂度:

插入的过程中递归了 \(O(\log n)\) 层,而每次 \(\texttt{push\_up}\) 操作的时间复杂度同为 \(O(\log n)\),则单次插入的时间复杂度为 \(O(\log ^ 2 n)\)

完整代码:

#include <bits/extc++.h>

#define inline __always_inline
#define lson(u) (son[(u)][0])
#define rson(u) (son[(u)][1])
template <typename T> inline void chkmin(T &x, const T &y) { if (x < y) x = y; }
template <typename T> inline void chkmax(T &x, const T &y) { if (x < y) x = y; }
template <typename T> inline void read(T &x)
{
	char ch; bool flag = false;
	for (ch = getchar(); !isdigit(ch); ch = getchar()) flag = ch == '-';
	for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
	if (flag) x = -x;
}
const int MaxN = 1e5 + 5, MaxSGT = MaxN * 2, inf = 1e9;

struct segment_tree_t
{
	int son[MaxSGT][2], cnt[MaxSGT], top = 2;
	double max[MaxSGT];
	void build(int u, int l, int r)
	{
		if (r - l == 1) return;
		build(lson(u) = top++, l, l + r >> 1);
		build(rson(u) = top++, l + r >> 1, r);
	}
	int sum(int u, int l, int r, double k)
	{
		if (r - l == 1) return max[u] > k;
		if (max[lson(u)] <= k)
			return sum(rson(u), l + r >> 1, r, k);
		else return cnt[u] - cnt[lson(u)] + sum(lson(u), l, l + r >> 1, k);
	}
	void push_up(int u, int l, int r)
	{
		max[u] = std::max(max[lson(u)], max[rson(u)]);
		cnt[u] = cnt[lson(u)] + sum(rson(u), l + r >> 1, r, max[lson(u)]);
	}
	void set(int u, int l, int r, int x, double k)
	{
		if (r - l == 1) return cnt[u] = 1, max[u] = k, void();
		int mid = l + r >> 1;
		if (x < mid) set(lson(u), l, mid, x, k);
		else set(rson(u), mid, r, x, k);
		push_up(u, l, r);
	}
} tree;

int n, m, x, y;
int main()
{
	read(n), read(m);
	tree.build(1, 1, n + 1);
	for (int i = 0; i < m; i++)
	{
		read(x), read(y);
		tree.set(1, 1, n + 1, x, 1.0 * y / x);
		printf("%d\n", tree.cnt[1]);
	}
	return 0;
}

其实本题也可以使用吉司机线段树以 \(O(n \log n)\) 的时间复杂度完成。

例题:

线段树二分

[PA2015] Siano

#include <bits/extc++.h>

#define inline __always_inline
#define lson(u) (son[(u)][0])
#define rson(u) (son[(u)][1])
template <typename T> inline void chkmax(T &x, const T &y) { if (x < y) x = y; }
template <typename T> inline void chkmin(T &x, const T &y) { if (x > y) x = y; }
template <typename T> inline void read(T &x)
{
	char ch;
	for (ch = getchar(); !isdigit(ch); ch = getchar());
	for (x = 0; isdigit(ch); ch = getchar())	x = x * 10 + ch - '0';
}
const int MaxN = 5e5 + 5, MaxSGT = MaxN * 2;

int n, m;
int64_t v[MaxN], d, b, cur = 0;
struct segment_tree_t
{
	int son[MaxSGT][2], top = 2;
	int64_t max[MaxSGT], sum[MaxSGT], set[MaxSGT], add[MaxSGT];
	char is_add[MaxSGT], is_set[MaxSGT];
	inline void push_up(int u)
	{
		max[u] = std::max(max[lson(u)], max[rson(u)]);
		sum[u] = sum[lson(u)] + sum[rson(u)];
	}
	void build(int u, int l, int r)
	{
		if (r - l == 1) return;
		build(lson(u) = top++, l, l + r >> 1);
		build(rson(u) = top++, l + r >> 1, r);
		push_up(u);
	}
	inline void clear_add(int u) { add[u] = is_add[u] = 0; }
	inline void push_add(int u, int l, int r, int64_t x)
	{
		add[u] += x; max[u] += x * (v[r - 1] - v[r - 2]);
		sum[u] += (v[r - 1] - v[l - 1]) * x;
		is_add[u] = 1;
	}
	inline void clear_set(int u) { is_set[u] = 0; }
	inline void push_set(int u, int l, int r, int64_t x)
	{
		clear_add(u);
		sum[u] = (max[u] = set[u] = x) * (r - l);
		is_set[u] = 1;
	}
	void push_down(int u, int l, int r)
	{
		int mid = l + r >> 1;
		if (is_set[u]) push_set(lson(u), l, mid, set[u]), push_set(rson(u), mid, r, set[u]), clear_set(u);
		if (is_add[u]) push_add(lson(u), l, mid, add[u]), push_add(rson(u), mid, r, add[u]), clear_add(u);
	}
	int find(int u, int l, int r, int64_t k)
	{
		if (r - l == 1) return l;
		push_down(u, l, r);
		if (k <= max[lson(u)]) return find(lson(u), l, l + r >> 1, k);
		else return find(rson(u), l + r >> 1, r, k);
	}
	void Set(int u, int l, int r, int pl, int pr, int64_t x)
	{
		if (pl <= l && r <= pr) return push_set(u, l, r, x);
		int mid = l + r >> 1; push_down(u, l, r);
		if (pl < mid) Set(lson(u), l, mid, pl, pr, x);
		if (mid < pr) Set(rson(u), mid, r, pl, pr, x);
		push_up(u);
	}
} tree;

int main()
{
	read(n), read(m);
	for (int i = 1; i <= n; i++) read(v[i]);
	std::sort(v + 1, v + n + 1);
	for (int i = 1; i <= n; i++) v[i] += v[i - 1];
	tree.build(1, 1, n + 1);
	for (int i = 0; i < m; i++)
	{
		read(d), read(b);
		tree.push_add(1, 1, n + 1, d - cur), cur = d;
		if (b >= tree.max[1])
		{
			printf("0\n");
			continue;
		}
		int l = tree.find(1, 1, n + 1, b);
		int64_t oi = tree.sum[1];
		tree.Set(1, 1, n + 1, l, n + 1, b);
		int64_t afo = tree.sum[1];
		printf("%ld\n", oi - afo);
	}
	return 0;
}	// I don't wanna AFO.
posted @ 2024-10-12 10:58  yiming564  阅读(40)  评论(0)    收藏  举报