线段树入门

本文最后更新于:2023年12月4日 晚上

简介

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

线段树是中常用的用来维护区间信息的数据结构。对于其每一个子节点而言,都表示整个序列中的一段子区间;对于每个叶子节点而言,都表示序列中的单个元素信息;子节点不断向自己的父亲节点传递信息,而父节点存储的信息则是他的每一个子节点信息的整合。

线段树的基本形态

线段树每个区间所管理的关系图
线段树关系图

线段树的构建

我们可以通过递归的方式构建出一棵二叉树。参考代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void build(int s, int t, int p) {
// 对 [s,t] 区间建立线段树,当前根的编号为 p
if (s == t) {
// s==t时候,说明为叶子节点
d[p] = a[s];
return;
}
int m = s + ((t - s) >> 1);
// 去s-t的中点分割
// 移位运算符的优先级小于加减法,所以加上括号
// 如果写成 (s + t) >> 1 可能会超出 int 范围
build(s, m, p * 2), build(m + 1, t, p * 2 + 1);
// 递归对左右区间建树
d[p] = d[p * 2] + d[(p * 2) + 1];
}

线段树的区间查询

再回到开头的图,你会发现,查询[1,10]之间的节点,只需要查询其根节点的值即可。

如果要查询的区间为 [3,5],此时就不能直接获取区间的值,但是 [3,5] 可以拆成 [3,3] 和 [4,5],可以通过合并这两个区间的答案来求得这个区间的答案。

线段树关系图

参考代码

1
2
3
4
5
6
7
8
9
10
11
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 - s) >> 1), sum = 0;
if (l <= m) sum += getsum(l, r, s, m, p * 2);
// 如果左儿子代表的区间 [s, m] 与询问区间有交集, 则递归查询左儿子
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
// 如果右儿子代表的区间 [m + 1, t] 与询问区间有交集, 则递归查询右儿子
return sum;
}

线段树的区间修改与懒惰标记

如果要求修改区间 [l,r],把所有包含在区间 [l,r] 中的节点都遍历一次、修改一次,时间复杂度无法承受。我们这里引入做 「懒惰标记」。通过延迟对节点信息的更改,从而减少可能不必要的操作次数。每次执行修改时,我们通过打标记的方法表明该节点对应的区间在某一次操作中被更改,但不更新该节点的子节点的信息。实质性的修改则在下一次访问带有标记的节点时才进行。

区间修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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) {
// 区间内有几个在范围内的点*val值
d[p] += (t - s + 1) * c, b[p] += c;
return;
} // 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
int m = s + ((t - s) >> 1);
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];
}

区间查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 - s) >> 1);
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;
}

一些优化

一些优化
这里总结几个线段树的优化:

  1. 在叶子节点处无需下放懒惰标记,所以懒惰标记可以不下传到叶子节点。

  2. 下放懒惰标记可以写一个专门的函数 pushdown,从儿子节点更新当前节点也可以写一个专门的函数 maintain(或者对称地用 pushup),降低代码编写难度。

  3. 标记永久化:如果确定懒惰标记不会在中途被加到溢出(即超过了该类型数据所能表示的最大范围),那么就可以将标记永久化。标记永久化可以避免下传懒惰标记,只需在进行询问时把标记的影响加到答案当中,从而降低程序常数。具体如何处理与题目特性相关,需结合题目来写。这也是树套树和可持久化数据结构中会用到的一种技巧。

例题

https://www.luogu.com.cn/problem/P3372

具体来说就是在线段树上完成一下操作

  1. 将某区间每一个数加上k。
  2. 求出某区间每一个数的和。

使用模板的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

template <typename T>
class SegTreeLazyRangeAdd
{
vector<T> tree, lazy;
vector<T> *arr;
int n, root, n4, end;

void maintain(int cl, int cr, int p)
{
int cm = cl + (cr - cl) / 2;
if (cl != cr && lazy[p])
{
lazy[p * 2] += lazy[p];
lazy[p * 2 + 1] += lazy[p];
tree[p * 2] += lazy[p] * (cm - cl + 1);
tree[p * 2 + 1] += lazy[p] * (cr - cm);
lazy[p] = 0;
}
}

T range_sum(int l, int r, int cl, int cr, int p)
{
if (l <= cl && cr <= r)
return tree[p];
int m = cl + (cr - cl) / 2;
T sum = 0;
maintain(cl, cr, p);
if (l <= m)
sum += range_sum(l, r, cl, m, p * 2);
if (r > m)
sum += range_sum(l, r, m + 1, cr, p * 2 + 1);
return sum;
}

void range_add(int l, int r, T val, int cl, int cr, int p)
{
if (l <= cl && cr <= r)
{
lazy[p] += val;
tree[p] += (cr - cl + 1) * val;
return;
}
int m = cl + (cr - cl) / 2;
maintain(cl, cr, p);
if (l <= m)
range_add(l, r, val, cl, m, p * 2);
if (r > m)
range_add(l, r, val, m + 1, cr, p * 2 + 1);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
}

void build(int s, int t, int p)
{
if (s == t)
{
tree[p] = (*arr)[s];
return;
}
int m = s + (t - s) / 2;
build(s, m, p * 2);
build(m + 1, t, p * 2 + 1);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
}

public:
explicit SegTreeLazyRangeAdd<T>(vector<T> v)
{
n = v.size();
n4 = n * 4;
tree = vector<T>(n4, 0);
lazy = vector<T>(n4, 0);
arr = &v;
end = n - 1;
root = 1;
build(0, end, 1);
arr = nullptr;
}

void show(int p, int depth = 0)
{
if (p > n4 || tree[p] == 0)
return;
show(p * 2, depth + 1);
for (int i = 0; i < depth; ++i)
putchar('\t');
printf("%d:%d\n", tree[p], lazy[p]);
show(p * 2 + 1, depth + 1);
}

T range_sum(int l, int r) { return range_sum(l, r, 0, end, root); }

void range_add(int l, int r, int val) { range_add(l, r, val, 0, end, root); }
};

int main()
{
ll n, m;
scanf("%lld%lld", &n, &m);
vector<ll> ans;
ll x;
for (ll i = 0; i < n; i++)
{
scanf("%lld", &x);
ans.push_back(x);
}
SegTreeLazyRangeAdd<ll> st(ans);
for (ll i = 0; i < m; i++)
{
ll op;
scanf("%lld", &op);
if (op == 1)
{
ll x, y, k;
scanf("%lld%lld%lld", &x, &y, &k);
st.range_add(x - 1, y - 1, k);
}
else
{
ll x, y;
scanf("%lld %lld", &x, &y);
printf("%lld\n", st.range_sum(x - 1, y - 1));
}
}
return 0;
}

线段树入门
https://www.liahnu.top/2023/03/28/线段树入门/
作者
liahnu
发布于
2023年3月28日
许可协议