线段树 - 多组图带你从头到尾彻底理解线段树
线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。
相关阅读:树状数组
线段树可以在
线段树维护的信息,需要满足可加性,即能以可以接受的速度合并信息和修改信息,包括在使用懒惰标记时,标记也要满足可加性(例如取模就不满足可加性,对
线段树
线段树的基本结构与建树
线段树将每个长度不为
有个大小为
图中
通过观察不难发现,
具体要怎么用代码实现呢?
我们继续观察,有没有发现如果
思路如下:
此处给出 C++ 的代码实现,可参考注释理解:
void build(int s, int t, int p) {
// 对 [s,t] 区间建立线段树,当前根的编号为 p
if (s == t) {
d[p] = a[s];
return;
}
int m = (s + t) / 2;
build(s, m, p * 2), build(m + 1, t, p * 2 + 1);//递归到左右子树
// 递归对左右区间建树
d[p] = d[p * 2] + d[(p * 2) + 1];
}
关于线段树的空间:如果采用堆式存储(
分析:容易知道线段树的深度是
线段树的区间查询
区间查询,比如求区间
以上面这张图为例,如果要查询区间
如果要查询的区间为
一般地,如果要查询的区间是
此处给出 C++ 的代码实现,可参考注释理解:
int getsum(int l, int r, int s, int t, int p) {
// [l,r] 为查询区间,[s,t] 为当前节点包含的区间,p 为当前节点的编号
if (l <= s && t <= r)
return d[p]; // 当前区间为询问区间的子集时直接返回当前区间的和
int m = (s + t) / 2, sum = 0;
if (l <= m) sum += getsum(l, r, s, m, p * 2);
// 如果左儿子代表的区间 [l,m] 与询问区间有交集,则递归查询左儿子
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
// 如果右儿子代表的区间 [m+1,r] 与询问区间有交集,则递归查询右儿子
return sum;
}
线段树的区间修改与懒惰标记
如果要求修改区间
我们设一个数组
A 有两个儿子,一个是 B,一个是 C。
有一天 A 要建一个新房子,没钱。刚好过年嘛,有人要给 B 和 C 红包,两个红包的钱数相同都是
元,然而因为 A 是父亲所以红包肯定是先塞给 A 咯~ 理论上来讲 A 应该把两个红包分别给 B 和 C,但是……缺钱嘛,A 就把红包偷偷收到自己口袋里了。
A 高兴地说:「我现在有
份红包了!我又多了 元了!哈哈哈~」 但是 A 知道,如果他不把红包给 B 和 C,那 B 和 C 肯定会不爽然后导致家庭矛盾最后崩溃,所以 A 对儿子 B 和 C 说:「我欠你们每人
份 元的红包,下次有新红包给过来的时候再给你们!这里我先做下记录……嗯……我欠你们各 元……」 儿子 B、C 有点恼怒:「可是如果有同学问起我们我们收到了多少红包咋办?你把我们的红包都收了,我们还怎么装?」
父亲 A 赶忙说:「有同学问起来我就会给你们的!我欠条都写好了不会不算话的!」
这样 B、C 才放了心。
在这个故事中我们不难看出,A 就是父亲节点,B 和 C 是 A 的儿子节点,而且 B 和 C 是叶子节点,分别对应一个数组中的值(就是之前讲的数组
如图:
注:这里 D 表示当前节点的值(即所表示区间的区间和)。
为什么节点 A 的 D 是
如果这时候我们要查询区间
具体是这样操作(如图):
注:为什么是加上
由此我们可以得到,区间
区间修改(区间加上某个值):
void update(int l, int r, int c, int s, int t, int p) {
// [l,r] 为修改区间,c 为被修改的元素的变化量,[s,t] 为当前节点包含的区间,p
// 为当前节点的编号
if (l <= s && t <= r) {
d[p] += (t - s + 1) * c, b[p] += c;
return;
} // 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
int m = (s + t) / 2;
if (b[p] && s != t) {
// 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
b[p] = 0; // 清空当前节点的标记
}
if (l <= m) update(l, r, c, s, m, p * 2);
if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
d[p] = d[p * 2] + d[p * 2 + 1];
}
区间查询(区间求和):
int getsum(int l, int r, int s, int t, int p) {
// [l,r] 为查询区间,[s,t] 为当前节点包含的区间,p为当前节点的编号
if (l <= s && t <= r) return d[p];
// 当前区间为询问区间的子集时直接返回当前区间的和
int m = (s + t) / 2;
if (b[p]) {
// 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m),
b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
b[p] = 0; // 清空当前节点的标记
}
int sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p * 2);
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
return sum;
}
如果你是要实现区间修改为某一个值而不是加上某一个值的话,代码如下:
void update(int l, int r, int c, int s, int t, int p) {
if (l <= s && t <= r) {
d[p] = (t - s + 1) * c, b[p] = c;
return;
}
int m = (s + t) / 2;
if (b[p]) {
d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m),
b[p * 2] = b[p * 2 + 1] = b[p];
b[p] = 0;
}
if (l <= m) update(l, r, c, s, m, p * 2);
if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
d[p] = d[p * 2] + d[p * 2 + 1];
}
int getsum(int l, int r, int s, int t, int p) {
if (l <= s && t <= r) return d[p];
int m = (s + t) / 2;
if (b[p]) {
d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m),
b[p * 2] = b[p * 2 + 1] = b[p];
b[p] = 0;
}
int sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p * 2);
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
return sum;
}
一些优化
这里总结几个线段树的优化:
-
在叶子节点处无需下放懒惰标记,所以懒惰标记可以不下传到叶子节点。
-
下放懒惰标记可以写一个专门的函数
pushdown
,从儿子节点更新当前节点也可以写一个专门的函数maintain
(或者对称地用pushup
),降低代码编写难度。 -
标记永久化,如果确定懒惰标记不会在中途被加到溢出(即超过了该类型数据所能表示的最大范围),那么就可以将标记永久化。标记永久化可以避免下传懒惰标记,只需在进行询问时把标记的影响加到答案当中,从而降低程序常数。具体如何处理与题目特性相关,需结合题目来写。这也是树套树和可持久化数据结构中会用到的一种技巧。
线段树基础题推荐
<<
和 |
是位运算,n << 1 == n * 2
,n << 1 | 1 == n * 2 + 1
(再具体可以查看 blog )。
luogu P3372【模板】线段树 1
https://www.luogu.com.cn/problem/P3372
#include <iostream>
typedef long long LL;
LL n, a[100005], d[270000], b[270000];
void build(LL l, LL r, LL p) {
if (l == r) {
d[p] = a[l];
return;
}
LL m = (l + r) >> 1;
build(l, m, p << 1), build(m + 1, r, (p << 1) | 1);
d[p] = d[p << 1] + d[(p << 1) | 1];
}
void update(LL l, LL r, LL c, LL s, LL t, LL p) {
if (l <= s && t <= r) {
d[p] += (t - s + 1) * c, b[p] += c;
return;
}
LL m = (s + t) >> 1;
if (b[p])
d[p << 1] += b[p] * (m - s + 1), d[(p << 1) | 1] += b[p] * (t - m),
b[p << 1] += b[p], b[(p << 1) | 1] += b[p];
b[p] = 0;
if (l <= m) update(l, r, c, s, m, p << 1);
if (r > m) update(l, r, c, m + 1, t, (p << 1) | 1);
d[p] = d[p << 1] + d[(p << 1) | 1];
}
LL getsum(LL l, LL r, LL s, LL t, LL p) {
if (l <= s && t <= r) return d[p];
LL m = (s + t) >> 1;
if (b[p])
d[p << 1] += b[p] * (m - s + 1), d[(p << 1) | 1] += b[p] * (t - m),
b[p << 1] += b[p], b[(p << 1) | 1] += b[p];
b[p] = 0;
LL sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p << 1);
if (r > m) sum += getsum(l, r, m + 1, t, (p << 1) | 1);
return sum;
}
int main() {
std::ios::sync_with_stdio(0);
LL q, i1, i2, i3, i4;
std::cin >> n >> q;
for (LL i = 1; i <= n; i++) std::cin >> a[i];
build(1, n, 1);
while (q--) {
std::cin >> i1 >> i2 >> i3;
if (i1 == 2)
std::cout << getsum(i2, i3, 1, n, 1) << std::endl;
else
std::cin >> i4, update(i2, i3, i4, 1, n, 1);
}
return 0;
}
luogu P3373【模板】线段树 2
https://www.luogu.com.cn/problem/P3373
相比较于 P3372
,此题多了个区间乘法。
一个 tag
似乎应付不了了,那么来两个 tag
啊: add
和 mul
。
1. 区间加法
还是一样。
s[pos].add = (s[pos].add + k) % mod;
s[pos].sum = (s[pos].sum + k * (s[pos].r - s[pos].l + 1)) % mod;
2. 区间乘法
这里就有点不一样了。
先把 mul
和 sum
乘上 k
。
对于之前已经有的 add
,把它乘上 k
即可。在这里,我们把乘之后的值直接更新add的值。
你想, add
其实应该加到 sum
里面,所有乘上 k
后,运用乘法分配律, (sum + add) * k == sum * k + add * k
。
这样来实现 add
和 sum
有序进行。
s[pos].add = (s[pos].add * k) % mod;
s[pos].mul = (s[pos].mul * k) % mod;
s[pos].sum = (s[pos].sum * k) % mod;
3. pushdown的维护
现在要下传两个标记: add
和 mul
。
sum
:因为 add
之前已经乘过,所以在子孩子乘过 mul
后直接加就行。
mul
:直接乘。
add
:因为 add
的值是要包括乘之后的值,所以子孩子要先乘上 mul
。
s[pos << 1].sum = (s[pos << 1].sum * s[pos].mul + s[pos].add * (s[pos << 1].r - s[pos << 1].l + 1)) % mod;
s[pos << 1].mul = (s[pos << 1].mul * s[pos].mul) % mod;
s[pos << 1].add = (s[pos << 1].add * s[pos].mul + s[pos].add) % mod;
代码
#include<bits/stdc++.h>
using namespace std;
#define maxn 100010
typedef long long ll;
ll n, m, mod;
int a[maxn];
struct Segment_Tree {
ll sum, add, mul;
int l, r;
}s[maxn << 2];
void update(int pos) {
s[pos].sum = (s[pos << 1].sum + s[(pos << 1) | 1].sum) % mod;
return;
}
void pushdown(int pos) {//pushdown的维护
s[pos << 1].sum = (s[pos << 1].sum * s[pos].mul + s[pos].add * (s[pos << 1].r - s[pos << 1].l + 1)) % mod;
s[pos << 1 | 1].sum = (s[pos << 1 | 1].sum * s[pos].mul + s[pos].add * (s[pos << 1 | 1].r - s[pos << 1 | 1].l + 1)) % mod;
s[pos << 1].mul = (s[pos << 1].mul * s[pos].mul) % mod;
s[pos << 1 | 1].mul = (s[pos << 1 | 1].mul * s[pos].mul) % mod;
s[pos << 1].add = (s[pos << 1].add * s[pos].mul + s[pos].add) % mod;
s[pos << 1 | 1].add = (s[pos << 1 | 1].add * s[pos].mul + s[pos].add) % mod;
s[pos].add = 0;
s[pos].mul = 1;
return;
}
void build_tree(int pos, int l, int r) { //建树
s[pos].l = l;
s[pos].r = r;
s[pos].mul = 1;
if (l == r) {
s[pos].sum = a[l] % mod;
return;
}
int mid = (l + r) >> 1;
build_tree(pos << 1, l, mid);
build_tree(pos << 1 | 1, mid + 1, r);
update(pos);
return;
}
void ChangeMul(int pos, int x, int y, int k) { //区间乘法
if (x <= s[pos].l && s[pos].r <= y) {
s[pos].add = (s[pos].add * k) % mod;
s[pos].mul = (s[pos].mul * k) % mod;
s[pos].sum = (s[pos].sum * k) % mod;
return;
}
pushdown(pos);
int mid = (s[pos].l + s[pos].r) >> 1;
if (x <= mid) ChangeMul(pos << 1, x, y, k);
if (y > mid) ChangeMul(pos << 1 | 1, x, y, k);
update(pos);
return;
}
void ChangeAdd(int pos, int x, int y, int k) { //区间加法
if (x <= s[pos].l && s[pos].r <= y) {
s[pos].add = (s[pos].add + k) % mod;
s[pos].sum = (s[pos].sum + k * (s[pos].r - s[pos].l + 1)) % mod;
return;
}
pushdown(pos);
int mid = (s[pos].l + s[pos].r) >> 1;
if (x <= mid) ChangeAdd(pos << 1, x, y, k);
if (y > mid) ChangeAdd(pos << 1 | 1, x, y, k);
update(pos);
return;
}
ll AskRange(int pos, int x, int y) { //区间询问
if (x <= s[pos].l && s[pos].r <= y) {
return s[pos].sum;
}
pushdown(pos);
ll val = 0;
int mid = (s[pos].l + s[pos].r) >> 1;
if (x <= mid) val = (val + AskRange(pos << 1, x, y)) % mod;
if (y > mid) val = (val + AskRange(pos << 1 | 1, x, y)) % mod;
return val;
}
int main() {
//freopen("in.txt","r",stdin);
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
ll q, i1, i2, i3, i4;
cin >> n >> m >> mod;
for (int i = 1; i <= n; ++i)cin >> a[i];
build_tree(1, 1, n);
for (int i = 1; i <= m; ++i) {
cin >> i1 >> i2 >> i3;
if (i1 == 1)
cin>>i4, ChangeMul(1, i2, i3, i4);
if (i1 == 2)
cin >> i4, ChangeAdd(1, i2, i3, i4);
if (i1 == 3)
cout << AskRange(1, i2, i3) << endl;
}
return 0;
}
HihoCoder 1078 线段树的区间修改
https://cn.vjudge.net/problem/HihoCoder-1078
#include <iostream>
int n, a[100005], d[270000], b[270000];
void build(int l, int r, int p) {
if (l == r) {
d[p] = a[l];
return;
}
int m = (l + r) >> 1;
build(l, m, p << 1), build(m + 1, r, (p << 1) | 1);
d[p] = d[p << 1] + d[(p << 1) | 1];
}
void update(int l, int r, int c, int s, int t, int p) {
if (l <= s && t <= r) {
d[p] = (t - s + 1) * c, b[p] = c;
return;
}
int m = (s + t) >> 1;
if (b[p]) {
d[p << 1] = b[p] * (m - s + 1), d[(p << 1) | 1] = b[p] * (t - m);
b[p << 1] = b[(p << 1) | 1] = b[p];
b[p] = 0;
}
if (l <= m) update(l, r, c, s, m, p << 1);
if (r > m) update(l, r, c, m + 1, t, (p << 1) | 1);
d[p] = d[p << 1] + d[(p << 1) | 1];
}
int getsum(int l, int r, int s, int t, int p) {
if (l <= s && t <= r) return d[p];
int m = (s + t) >> 1;
if (b[p]) {
d[p << 1] = b[p] * (m - s + 1), d[(p << 1) | 1] = b[p] * (t - m);
b[p << 1] = b[(p << 1) | 1] = b[p];
b[p] = 0;
}
int sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p << 1);
if (r > m) sum += getsum(l, r, m + 1, t, (p << 1) | 1);
return sum;
}
int main() {
std::ios::sync_with_stdio(0);
std::cin >> n;
for (int i = 1; i <= n; i++) std::cin >> a[i];
build(1, n, 1);
int q, i1, i2, i3, i4;
std::cin >> q;
while (q--) {
std::cin >> i1 >> i2 >> i3;
if (i1 == 0)
std::cout << getsum(i2, i3, 1, n, 1) << endl;
else
std::cin >> i4, update(i2, i3, i4, 1, n, 1);
}
return 0;
}
2018 Multi-University Training Contest 5 Problem G. Glad You Came
http://acm.hdu.edu.cn/showproblem.php?pid=6356
维护一下每个区间的永久标记就可以了,最后在线段树上跑一边 dfs
统计结果即可。注意打标记的时候加个剪枝优化,否则会 T。
拓展 - 猫树
众所周知线段树可以支持高速查询某一段区间的信息和,比如区间最大子段和,区间和,区间矩阵的连乘积等等。
但是有一个问题在于普通线段树的区间询问在某些毒瘤的眼里可能还是有些慢了。
简单来说就是线段树建树的时候需要做
而所谓 "猫树" 就是一种不支持修改,仅仅支持快速区间询问的一种静态线段树。
构造一棵这样的静态线段树需要
在处理线性基这样特殊的信息的时候甚至可以将复杂度降至
原理
在查询
这个区间一定包含
显然,因为它既是
- 这个区间一定跨越
的中点
由于 lca
,这意味着
因此
有了这两个性质,我们就可以将询问的复杂度降至
实现
具体来讲我们建树的时候对于线段树树上的一个节点,设它代表的区间为
不同于传统线段树在这个节点里只保留
这样的话建树的复杂度为
下面是最关键的询问了~
如果我们询问的区间是
根据刚才的两个性质,
这意味这一个非常关键的事实是我们可以使用
而这个过程仅仅需要
不过我们好像忽略了点什么?
似乎求 lca
的复杂度似乎还不是
堆式建树
具体来将我们将这个序列补成 2 的整次幂,然后建线段树。
此时我们发现线段树上两个节点的 lca
编号,就是两个节点二进制编号的 lcp
。
稍作思考即可发现发现在 lcp(x,y)=x>>log[x^y]
。
所以我们预处理一个 log
数组即可轻松完成求 lca
的工作。
这样我们就完成了一个猫树。
由于建树的时候涉及到求前缀和和求后缀和,所以对于线性基这种虽然合并是
应用
经典问题:区间最大子段和
给出一个序列,多次询问区间的最大子段和。
这是一个经典的模型。不同于经典做法,我们只需要记录 prelprel、prerprer 为对应前(后)缀的最大子段和、最大前(后)缀和即可。
复杂度:
经典问题:NAND
给出一个序列,多次询问一个 xx 对一个区间中所有数按顺序依次 NAND 的结果。
NAND 没有结合律,因此我们要用一些经典的处理方式。NAND 是按位独立的,因此我们可以对每一位维护信息:如果这一位刚开始是 0,那么按顺序 NAND 了这个区间中的所有数后会变成什么;如果这一位是 1 那么会变成多少。用位运算可以优化为
使用猫树,即可直接支持
经典问题:区间凸包
参考
其它
文章开源在 Github - blog-articles,点击 Watch 即可订阅本博客。 若文章有错误,请在 Issues 中提出,我会及时回复,谢谢。
如果您觉得文章不错,或者在生活和工作中帮助到了您,不妨给个 Star,谢谢。
(文章完)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· 为DeepSeek添加本地知识库
· 精选4款基于.NET开源、功能强大的通讯调试工具
· DeepSeek智能编程
· 大模型工具KTransformer的安装
· [计算机/硬件/GPU] 显卡