线段树学习笔记

待补:扫描线,可持久化,树套树,李超线段树,线段树分治,势能线段树,树链剖分。

线段树学习笔记

线段树(Segment Tree)是一种基于分治思想的二叉树,能够高效的维护一些区间信息。与树状数组相比,线段树显得更加通用。

线段树是一棵完全二叉树。且根节点是唯一的,代表的区间是整个维护的范围,如 [1,n]。线段树的每一个节点都代表一个区间。对于一个编号为 x 节点,所代表的区间为 [l,r],并令 mid=l+r2。它的左儿子和右儿子的编号分别为 2x2x+1,维护的区间为 [l,mid][mid+1,r]。线段树的每一个叶节点都维护的是一个元区间,如 [x,x]

线段树有以下性质:
线段树的每个节点维护的区间的大小之和的数量级为 O(nlogn)
显然,线段树每向下走一层,区间长度都会缩减一半,则树高为 logn,而每层节点的并集最多为 [1,n](有可能最后一层不是)。所以长度为 nlogn

节点的数量级为 O(n)
对于每一层,显然节点个数都是上一层的两倍,则共有 i=1logn2i=2log(n)+11=2n 个节点,但考虑的一些边界问题,可将数组开至 4n,也方便程序的编写。

Tips:线段树用结构体书写程序效率更高,也更好调试。

例题 1

给定长度为 n 的数列 a,需要支持两种操作:

  1. 将下标在 [l,r] 的数加上 k
  2. 查询区间 [l,r] 的和。

数据范围:1n105|ai|1091lrn|k|109

线段树的建树

既然线段树使用递归定义的,那当然也要用递归实现啦。
先处理子节点,然后再将子节点的信息合并到当前节点上。
示例代码:

void pushup(int u) {
	tr[u].v = tr[u << 1].v + tr[u << 1 | 1].v;
}
void build(int u, int l, int r) {
	if (l == r) {
		tr[u].v = a[l];
		return;
	}
	int mid = (l + r) >> 1;
	build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
	pushup(u);
}

线段树的单点修改

操作形如 Cposk,即将下标为 pos 的数加上 k
可先在线段树上找到维护的区间为 [pos,pos] 的节点。然后将它到根节点的左右节点维护的信息更新。
由于每个节点维护的区间是确定的,递归修改即可,别忘了更新当前节点的信息。
示例代码:

void upd(int u, int l, int r, int k) {
	if (l == r) {
		tr[u].v += k;
		return;
	}
	int mid = (l + r) >> 1;
	if (k <= mid) upd(u << 1, l, mid, k);
	else upd(u << 1 | 1, mid + 1, r, k);
	pushup(u);
}

线段树的区间查询

操作形如 Qlr,即查询 i=lrai

先说过程,向下递归找区间,判断一下再向下走,就不会出现无交的情况了。如果是被完全包含就直接返回,否则继续递归。

一种严谨点的方法是分类讨论,但很麻烦。
考虑到每次向下递归时,那一层都只会有两个节点会继续向下递归,而其它节点要么无交,要么被包含。又因为树高为 O(logn),故时间复杂度也为 O(logn)

还有一种更感性的方法,。将区间在 n2 左边和右边的部分分别按照二进制分解,那会被分为 O(logn) 个区间。递归时树的深度又是 O(logn),所以时间复杂度也是 O(logn)
示例代码:

ll query(int u, int l, int r, int L, int R) {
	if (L <= l && r <= R) return tr[u].v;
	int mid = (l + r) >> 1;
	ll res = 0;
	if (L <= mid) res += query(u << 1, l, mid, L, R);
	if (mid < R) res += query(u << ! | 1, mid + 1, r, L, R);
	return res;
}

区间修改
虽然支持单点修改的线段树有点用,但用处还是十分局限,所以出现了延迟标记(也称懒标记或 lazy-tag)。
延迟标记的意思就是修改了过后,只要不再次更新或查询,就不会告诉子节点被修改,但是该维护信息是修改时就更新了的。
考虑到延迟标记,每次非完全包含时都需要下传标记,注意有时候需要开 long long。
用 pushdown 函数完成下传:

void pushdown(int u, int l, int r) {
	if (tr[u].tag) {
		int mid = (l + r) >> 1;
		tr[u << 1].tag += tr[u].tag, tr[u << 1 | 1].tag += tr[u].tag;
		tr[u << 1].v += (mid - l + 1) * tr[u].tag;
		tr[u << 1 | 1].v += (r - mid) * tr[u].tag;
	}
}

现在线段树的基本操作已经结束,模板题自己写去吧。

普通线段树的一些应用:

例题 2

给定长度为 n 且初始值均为 0 的数列,有 m 次操作。
要求支持:

  1. [l,r] 间的数全部取反,即 0 变为 11 变为 0
  2. 查询 [l,r] 内所有数的和。

数据范围:1n1051m105

容易发现,该问题的具有“可合并性”,考虑使用线段树维护。
每次修改时,将区间的值变为 rl+1v 即可,其中 v 为原来区间 1 的个数。

例题 3

给定长度为 n 的序列 a,有 m 次操作。
要求支持:

  1. [l,r] 的最大子段和。
  2. ap 修改为 k

数据范围:1n5×1041lrn1m5×104|ai|,|k|104,时限 330ms。

[L,R] 的最大子段和指 maxLlrRi=lrai
发现最大子段和具有可合并性,容易想到线段树。
每个节点维护的信息除了该区间的答案 mx,还需要包含区间左端点的最大子段和 mxl 和包含区间右端点的最大子段和 mxr 以及区间内所有数的和 sum,因为需要这些信息才能将子节点的信息合并到父节点。

每个信息的合并如下:

t[u].sum=t[2u].sum+t[2u+1].sum

t[u].mxl=max(t[2u].mxl,t[2u].sum+t[2u+1].mxl)

t[u].mxr=max(t[2u+1].mxr,t[2u+1].sum+t[2u].mxr)

t[u].mx=max(t[2u].mx,t[2u+1].mx,t[2u].mxr+t[2u+1].mxl)

修改的过程相似,不加赘述。

例题 4

给定长度为 n 的序列 a,有 m 次操作,要求支持:

  1. 将区间 [l,r] 的数开平方根后向下取整。
  2. 查询下标在 [l,r] 的数的和。

数据范围:1n,m1050<ai10121lrn

由于我太菜了,所以不讲解势能分析法。
发现不好对区间修改的延迟标记进行操作,但发现一个数最多进行 O(logn) 次操作就会变为 1,具体证明放在后面。
然后就很美妙了,对于每个修改直接向下暴力递归即可,当区间的最大值为 1 时,可以直接返回。
时间复杂度:O(nlognloglogw),其中 wmaxi=1nai

证明:将数 x 进行质因数分解得到 x=p1c1×p2c2×p3c3××pncn,发现每次开方,对于任意 i[1,n]ci 都会变为 ci2。那么进行 log(max{ci}) 次操作后 x 就会变为 1。而 max{ci} 最多为 logx,即当 x2 的幂次时有最大值。
证毕。

例题 5

给定长度为 n 的数列,和模数 p,有 m 次操作:

  1. 将区间内的每个数加上 k
  2. 将区间内每个数乘上 k
  3. 求序列的区间和对 p 取模的答案。

数据范围:1n,m105p=571373

一道简单的题,维护多个标记即可,注意标记下传的顺序。

权值线段树:
即维护值域的线段树。
例题 6

给定长度为 n 的数列 a
求出该 a 中逆序对的数量。
其中逆序对指满足 i<jai>aj 的二元组 (i,j) 的数量。

数据范围:1n105|ai|109

既然以值域作为维护区间。肯定要先离散化。
从前往后扫描 a,建立一棵权值线段树(权值树状数组其实也可以)。令当前扫描到的是 ai,查询树中的小于 ai 的数的数量,然后再将 ai 插进线段树里面就行了。

动态开点线段树:
比如值域为 [1,109],每次插入一个数或查询小于这个数的元素个数,强制在线(不能用平衡树)。
如果直接建一棵权值线段树,空间直接起飞。但是发现有很多的节点都是没用的,于是就产生了动态开点线段树。即根据每次所需建立新的节点,只是不满足完全二叉树的编码方式了而已,记录一下左儿子和右儿子的编号即可。
有了动态开点线段树,就能使线段树进行可持久化操作了。

可持久化线段树:

例题 7

给定长度为 n 的数列 a,有 m 次操作,要求支持:
查询区间 [l,r] 的第 k 小值。

数据范围:1n,m2×1051lrn|ai|1091krl+1

考虑一个很暴力的想法?对于每一个点都建立一棵前缀的权值线段树,对于操作可以在两棵树上同时递归,因为将两棵树相减就得到了当前区间所构成的权值线段树。但显然,空间无法承受。发现相邻的两颗线段树只多有 logn 个节点维护的信息不同,就在原树基础上额外建 logn 个节点。

具体地,对于点 u 的左子节点会改变,u 所对应的新节点的编号为 newnode,则 tr[newnode].rson=tr[u].rson,先将 u 的左子节点复制给 newnode 的左子节点(因为原来的信息是不会变的,所以要先复制,才能进行更改,毕竟直接在原树上修改的话也需要这个信息)。
然后实现就很简单了,注意单次修改要增加 logn(大约 20 个节点),但是空间比较宽裕,可以开成 N×25,不然有可能会有奇怪问题?

核心代码:

void change(int ltr, int &x, int l, int r, int k, int v) {
	if (!x) x = ++cnt;
	if (l == r) {
		tr[x].v += v;
		return;
	}
	int mid = l + r >> 1;
	if (k <= mid) {
		tr[x].rs = tr[ltr].rs;
		tr[x].ls = ++cnt;
		tr[tr[x].ls] = tr[tr[ltr].ls];
		change(tr[ltr].ls, tr[x].ls, l, mid, k, v);
	} else {
		tr[x].ls = tr[ltr].ls;
		tr[x].rs = ++cnt;
		tr[tr[x].rs] = tr[tr[ltr].rs];
		change(tr[ltr].rs, tr[x].rs, mid + 1, r, k, v); 
	}
	pushup(x);
}
int query(int x, int y, int l, int r, int k) {
	if (l == r) return l;
	int mid = l + r >> 1, tmp = tr[tr[y].ls].v - tr[tr[x].ls].v;
	if (tmp >= k) return query(tr[x].ls, tr[y].ls, l, mid, k);
	return query(tr[x].rs, tr[y].rs, mid + 1, r, k - tmp);
}

另外:可持久化线段树还有很多其他的功能,比如实现可持久化数组,还有可持久化并查集等等。

咕咕咕。

posted @   Pengzt  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示