简单线段树

一、什么是线段树?

  • 线段树是怎样的树形结构?

  线段树是一种二叉搜索树,每个结点都存储了一个区间,也可以理解成一个线段,你要从这些线段上进行搜索操作得到你想要的答案。

  • 线段树能够解决什么样的问题?

  线段树的适用范围很广,可以在线维护修改以及查询区间上的最值,求和。

  需要注意的是,线段树只可以维护满足结合律的信息。

  对于线段树来说,每次更新以及查询的时间复杂度为 \(O(\log n)\)

  • 线段树和其他 \(\texttt{RMQ}\) 算法的区别

  常用的解决 \(\texttt{RMQ}\) 问题有 \(\texttt{ST}\) 表,二者预处理时间都是 \(O(n\log n)\)

  但二者的区别在于线段树支持在线更新值,而 \(\texttt{ST}\) 表不支持在线操作。

二、线段树的原理

  线段树之所以称为“线段”树,是因为它的节点维护的信息是一个线段(即一个区间)的信息,而一个节点的两个儿子维护的信息是将当前节点维护线段(区间)分成两个线段,分别维护两个分成的线段(即子区间)的信息。

  而线段树的精髓在于:根据结合律,通过儿子节点(子区间)的信息合并得到更大区间的信息。

三、线段树的基本操作

1. 建树

建树包括三个要点:结点存什么结点下标是什么如何建树

image

  上图是关于 \(a[1\cdots6] = \{1,8,6,4,3,5\}\) 的区间最大值线段树,其中红色数字代表区间,蓝色数字代表当前区间最值。

  • 节点存什么

  可以发现,每个叶子结点的值就是数组的值,每个非叶子结点的度都为二,且左右两个孩子分别存储父亲一半的区间。每个父亲的存储的值也就是两个孩子存储的值的最大值

  • 结点下标是什么

  接着,我们采用完全二叉树的存储方法。即对于一个父节点 \(i\) 来说,它的左右儿子分别为 \(2i,2i+1\)

  • 如何建树

  故此,在建树的时候,空间要开到 \(4n\) 以防止 RE。

  建树时每次递归就要先判断 \(l\) 是否等于 \(r\),等于就说明是叶子节点,也就是区间是 \([l,l]\),直接赋值成 \(a[l]\),再返回。

  否则就递归构造左儿子结点和递归构造右儿子结点,最后更新父节点。

inline void PushUp(int x) {
	sum[x] = sum[x << 1] + sum[x << 1 | 1];
}//由于结合律,所以根据两儿子的信息可以合并成父亲的信息
inline void Build(int l, int r, int x) {
	if(l == r) {
		sum[x] = a[l];
		return ;
	}
	int mid = l + r >> 1;
	Build(l, mid, x << 1);
	Build(mid + 1, r, x << 1 | 1);
	PushUp(x);
}

(注意,示例代码均为线段树 1 代码)

2、区间查询

image

  我们继续观察这棵线段树。

  现在我要查询 \([2,5]\) 区间的最值,如何处理呢?

  显然,此区间无法直接通过某一个节点的值直接求出;那么我们来看看有哪些区间被此区间包含。

  共有 \(5\) 个区间(黄色部分),而且我们可以发现 \([4,5]\) 这个区间已经包含了两个子树的信息(\([4,4],[5,5]\)),所以我们需要查询的区间只有三个,分别是 \([2,2],[3,3],[4,5]\)

  我们还是从根节点开始往下递归,如果当前结点是被要查询的区间包含了的,则返回这个结点的信息,这样从根节点往下递归,时间复杂度也是 \(O(\log n)\)

inline int Query(int L, int R, int l, int r, int x) {
	if(L <= l && r <= R) return sum[x];
	int mid = l + r >> 1;
	int ans = 0;
	if(L <= mid) ans += Query(L, R, l, mid, x << 1);
	if(R > mid) ans += Query(L, R, mid + 1, r, x << 1 | 1);
	return ans;
}

3、单点修改

  从根节点递归去找 \(val\),找到了就返回,并再返回的一路上不断更新其父节点的信息即可。

  基本等于二分查找。

  (代码不用给了吧)

4、区间修改——懒标记(Lazytag)

  由于线段树需要支持区间修改,所以我们需要引入懒标记,使修改在 \(\log n\) 的时间复杂度内完成。

  懒标记的作用是记录当前区间已经修改,但子区间并未修改的信息。

  由于我们虽然修改了当前区间的信息,但并未修改子区间的信息,所以我们需要记录懒标记,并在需要递归到子区间之前将懒标记下推到子区间。

  设当前结点对应区间 \([l, r]\),待更新区间 \([a, b]\)

  当 \(a ≤ l ≤ r ≤ b\),即 \([l, r]∈[a,b]\) 时,不再向下更新,仅更新当前结点,并在该结点加上懒标记,当必须得更新/查询该结点的左右子结点时,再利用懒标记的记录向下更新(pushdown)——懒标记也要向下传递,然后移除该结点的懒标记。

image

  我们仍然观察这棵线段树。

  现在我要让 \([1,3]\) 集体加一。那么从根节点出发,即区间 \([1,6]\),发现这个区间并不是 \([1,3]\) 的“子集”,所以往下递归。下移到左子树 \([1,3]\),发现这个区间已经是 \([1,3]\) 的“子集” 了,那么我们在这个节点上加一个 lazytag,即 \(lazy[2]=1\),然后更新当前值。注意再递归回去的时候顺便更新根节点的值。

  现在我要让 \([2,3]\) 集体加二。

  依然先从根节点出发。转移到它的左子树 \([1,3]\)

  这一次,我们发现这个点无法满足是 \([2,3]\) 的“子集”,那么接着向下递归。但是,在递归之前,我们要将此节点的 lazytag 下传。即 \(lazy[4]+1,lazy[5]+1\),然后把当前 lazytag 清零,即 \(lazy[2]=0\)

  继续,下传到 \([1,2]\),同上,下传 lazytag 和清空;下传到 \([3,3]\),发现 \([3,3]\)\([2,3]\) 的“子集”,那么结束。

  最后,由 \([1,2]\) 下传到 \([2,2]\),修改结束。

inline void PushDown(int x, int ln, int rn) {
	if(add[x]) {
		add[x << 1] += add[x];
		add[x << 1 | 1] += add[x];
		sum[x << 1] += add[x] * ln;
		sum[x << 1 | 1] += add[x] * rn;
		add[x] = 0;
	}
}
inline void Update(int L, int R, int C, int l, int r, int x) {
	if(L <= l && R >= r) {
		sum[x] += C * (r - l + 1);//注意由于是求和,所以当前节点加的是包含区间长度*价值
		add[x] += C;
		return ;
	}
	int mid = l + r >> 1;
	PushDown(x, mid - l + 1, r - mid);
	if(L <= mid) Update(L, R, C, l, mid, x << 1);
	if(R > mid) Update(L, R, C, mid + 1, r, x << 1 | 1);
	PushUp(x);
}

5、区间查询(lazytag)

在前面的基础上加上 pushdown 即可。

四、例题

P3372 【模板】线段树 1

模板题。包括了区间修改和区间查询。代码在上面已经给出。

P1438 无聊的数列

显然不可以直接用线段树维护,因为区间加的不是同一个值。

再看一下题目发现,题目给出的是一个等差数列,也就是说,累加序列的相邻两项的差是相等的。

不难想到差分。这样我们就可以把区间加的数变为相等的数,可以用线段树维护了。再看查询是单点查询,也就是查询差分数组的前缀和,线段树普通区间查询即可。

P4588 [TJOI2018] 数学计算

这题乍一看和线段树没什么关系,疑似是可以直接计算的。

但是我们会发现,如果直接模拟题意,高精会爆空间,如果写逆元这题模数不是质数,直接计算肯定不行。

所以再仔细看题目:

对于每一个类型 1 的操作至多会被除一次

也就是说,当计算除法时,就相当于少乘一个乘数,即每次除法操作就相当于将某乘数变成 \(1\)

而每个乘法操作就是多一个乘数,即把乘以一个 \(1\) 改为乘以某个数。

而输出的数就是所有乘数之积,包括 \(1\) 在内。

单点修改,区间查询,用线段树维护区间积即可。

P4145 上帝造题的七分钟 2 / 花神游历各国

小清新线段树。

开方有 \(2\) 个特殊值:\(0,1\)。对于这两个数,开方无实际意义。

题目中给到的 \(a_i\) 值是 \(10^{12}\),在开 \(6\) 次方后就会变成 \(1\)

所以线段树中再记录区间最大值,当最大值是 \(1\) 时,就可以不用开方。

CF1263E Editor

经典括号问题。

对于一个括号串,我们有一条基本规则:

对于任意前缀,左括号数 \(\geq\) 右括号数,且两括号总数相等。

满足此条件,即可称此括号串为合法括号串。

在本题中,我们不妨设左括号为 \(1\),右括号为 \(-1\),普通字符为 \(0\)。这样的话,刚才的判断条件就转化成了如下式子。

\[ \begin{cases} \forall sum_i,sum_i \geq0\\ sum_n=0 \end{cases} \]

其中 \(sum_i\) 表示前缀和数组。

然后再来观察这样的定义下“最深嵌套”是什么。

从左往右每出现一个左括号就会使嵌套层数增加,而每出现一个右括号就会使嵌套层数减少,因此最深嵌套层数就是最大的前缀和。

线段树维护区间和,区间 min/max 即可。

P2894 [USACO08FEB] Hotel G

这题有区间修改,显然是线段树一类的数据结构来维护。

题面提到,要求连续的空房长度,显然需要用到较为复杂的维护。

考虑维护三个值:区间最长连续空房长度,区间最长连续空房前缀,区间最长连续空房后缀。

显然上传的时候,区间最长空房可以根据儿子的区间最长空房以及左儿子的最长后缀+右儿子的最长前缀的最大值转移。

区间最长前缀可以根据左儿子最长前缀(如果左儿子全是空房可以根据左儿子区间长度+右儿子最长前缀)转移。

区间最长后缀同理可得。

然后我们考虑如何求得长度 \(\geq x\) 的最左区间的左端点。

方法 1:考虑二分右端点,比较 \([1,mid]\) 区间的最长空房长度与 \(x\) 的大小。

方法 2:递归查找,判断线段树当前区间左儿子最长空房与 \(x\) 的大小,如果左儿子最长空房 \(≥x\),就递归左儿子;如果左儿子最长后缀+右儿子最长前缀 \(≥x\),就返回左儿子最长后缀的开始位置;否则递归右儿子。

方法 1 时间复杂度为 \(O(n \log^2 n)\),方法 2 时间复杂度为 \(O(n \log n)\),均可通过此题。

SP1043 GSS1 - Can you answer these queries I

经典线段树维护最大区间子段和,P4513 小白逛公园 的弱化版。

定义:

struct kkk{
    int val;	//表示该区间的权值和
    int left;	//表示该区间的前缀最大和
    int right;	//表示该区间的后缀最大和
    int middle;	//表示该区间的最大子段和
    kkk(){val=left=right=middle=0;} //清0
}tree[maxn];

大家都应知道,线段树基本原理,那么最大子段和放在线段树上,其实就是两个区间的合并时怎么将区间关系,pushup 区间的问题。

下面给出两个区间合并的方式:合并的区间为 \(res\)

合并保证 \(x\) 是左区间,\(y\) 是右区间,\(x,y\) 相邻。

首先是 \(val\) 的合并,很简单,区间 \(x\)\(val+\) 区间 \(y\)\(val\)

res.val=x.val+y.val;

然后是 \(left\) 的合并,前缀最大和只有两种情况,要么是 \(x\) 区间的前缀最大和,要么是 \(x\) 的权值和 \(y\) 的前缀最大和。结果是这两种情况的 \(max\) 值。

那么 \(right\) 的合并也差不多,要么是 \(y\) 区间的后缀最大和,要么是 \(y\) 的权值 \(+x\) 的后缀最大和。

至于 \(middle\) 就分几种情况:

1.\(x\) 区间的 \(middle\)
2.\(y\) 区间的 \(middle\)
3.\(x\) 区间的后缀最大和 \(+y\) 区间的前缀最大和

答案即为 \(middle\)

GSS3 - Can you answer these queries III

带修区间最大子段和。

只要在 GSS1 的基础上加上单点修改即可。基本和小白逛公园一致。

P1253 扶苏的问题

简单的线段树模板题。

使用两个 tag,t1 表示区间赋值,t2 表示区间加。加法打标记时若赋值标记存在则直接给赋值标记加上对应值,不修改加标记,否则修改加标记;打赋值标记时先清空加标记即可。

在实现时,区间赋值和区间加的两个 update 函数找节点的过程是相同的,区别只在 make_tag 过程。因此可以把这两个操作的 update 函数写成一个,另加一个参数表示操作类型是赋值还是加法即可。

P3870 [TJOI2009] 开关

我们令灯关着的时候是 \(0\),开着的时候是 \(1\)

初始所有数都是 \(0\)

题目要求支持两种操作,一种是区间异或 \(1\),还有一种是区间查询1的个数。

考虑到所有数中只有 \(0\)\(1\)

所以查询可以改为查询区间和。

区间异或 \(1\) 也可以从区间和的角度取考虑问题。

假设我们有 \(k\)\(1\),区间长度为 \(l\)

那么显然取反后有 \(l-k\)\(1\)

所以 \(tree_x = (r-l+1)-tree_x\)

P1471 方差

我们把方差公式展开。

image

显然区间平均值是好求的,那么我们重点观察这个区间平方和怎么求。

设区间加了 \(k\)

\[ \begin{cases} sum_1 = x_1 + x_2 + \cdots + x_n\\ sum_2 = x_1 ^ 2 + x_2 ^ 2 + \cdots + x_n ^ 2 \end{cases} \]

那么再次展开平方。

\(sum_2 = sum_2 + 2k\cdot sum_1 + nk^2\)

这个就可以线段树维护了。

P1558 色板游戏

posted @ 2023-12-17 08:46  The_cosmos  阅读(55)  评论(0编辑  收藏  举报