线段树是基于分治思想的二叉树,用来维护区间信息(区间和,区间最值,区间GCD等),可以在log(n)的时间内执行区间修改和区间查询。
线段树中每个叶子结点存储元素本身,非叶子结点存储区间内元素的统计。
(视频学习:【C02【模板】线段树+懒标记 Luogu P3372 线段树 1】https://www.bilibili.com/video/BV1G34y1L7b3?vd_source=4c9eb38d8205116069b961c84f64c958)
线段树结构
1、结点数组tr[]
结构体包含三个变量:l, r, sum
l,r存区间的左右端点,sum存区间和
2、递归建树
父节点编号为p
左孩子编号为 2 ∗ p 2*p 2∗p (p<<1),右孩子编号我 2 ∗ p + 1 2 * p + 1 2∗p+1 (p<<1|1)
3、代码实现
#include<iostream>
#define lc p<<1
#define rc p<<1|1
#define N 500005
using namespace std;
int n, w[N], ns;
struct node {
int l, r, sum;
}tr[N*4]; // N*4大小数组范围,如果想要了解,可以看上方链接视频
void build (int p, int l, int r) {
ns++;
tr[p] = {l, r, w[l]}; // 一开始对于前面的结点没有实质性影响,对于叶子节点刚好吻合,而在前面的结点可以通过叶子节点或后面结点递归回溯求出来
if(l == r){
cout << tr[p].l << ' ' << tr[p].r << ' ' << tr[p].sum << '\n';
return;
}
int m = l + r >> 1;
build(lc, l, m);
build(rc, m + 1, r);
tr[p].sum = tr[lc].sum + tr[rc].sum;
cout << tr[p].l << ' ' << tr[p].r << ' ' << tr[p].sum << '\n';
}
int main() {
int n; cin >> n;
for(int i = 1; i <= n; i++) cin >> w[i];
build(1, 1, n);
return 0;
}
如上代码运行结果,可以看出与上面图中树结构中每个结点相同
线段树操作
一、点修改
回顾下树状数组中更新某个点的值,需要通过+lowbit操作来维护t数组,但是我们这里使用线性数组tr[]来实现线段树这种二叉树结构,其中每个结点编号与原数组下标并没有太大关系,原数组的下标信息被存储在了结点内(l, r, sum),因此对原数组修改我们需要从树的根节点进行二分依次修改。
void update(int p, int x, int k) { // 对结点p进行修改,将原数组x位置上加上k值
if(tr[p].l == x && tr[p].r == x) {
tr[p].sum += k;
return;
}
int m = tr[p].l + tr[p].r >> 1;
if(m >= x) update(lc, x, k); // 这里是≥是因为在建树过程中左子树建立是包含m的,右子树是从m+1开始,所以对lc的判定要加上=
else update(rc, x, k);
tr[p].sum = tr[lc].sum + tr[rc].sum;
}
二、区间查询
int queue(int p, int x, int y) { //查询[x, y]区间和,p表示当前查询结点, 函数有返回值
cout << "入" << p << '\n';
if(x <= tr[p].l && y >= tr[p].r) {
cout << "回 " << p;
cout << " re " << tr[p].sum << '\n';
return tr[p].sum;
}
int m = tr[p].l + tr[p].r >> 1;
int ans = 0;
if(x <= m) ans += queue(lc, x, y);
if(y > m) ans += queue(rc, x, y);
cout << "回 " << p;
cout << " ans " << ans << " re " << ans << '\n';
return ans;
}
三、区间修改
对于区间修改,我们很容易想到差分,但是差分适合多次区间修改后查找某个位置上的值,并不适合区间修改后查询某个区间的问题。这里可以用线段树懒标记来实现。
#include<bits/stdc++.h>
using namespace std;
#define lc p<<1
#define rc p<<1|1
#define N 50010
struct node{
int l, r, sum, add;
}tr[4*N];
int n, w[N];
void pushup(int p) {
tr[p].sum = tr[lc].sum + tr[rc].sum;
}
void pushdown(int p) {
if(tr[p].add) {
tr[lc].sum += (tr[lc].r - tr[lc].l + 1) * tr[p].add;
tr[rc].sum += (tr[rc].r - tr[rc].l + 1) * tr[p].add;
tr[lc].add += tr[p].add;
tr[rc].add += tr[p].add;
tr[p].add = 0;
}
}
void build(int p, int l, int r) {
tr[p] = {l, r, w[l], 0};
if(l == r) return;
int m = tr[p].l + tr[p].r >> 1;
build(lc, l, m);
build(rc, m + 1, r);
pushup(p);
}
void update(int p, int x, int y, int k) {
cout << "入" << p << '\n';
if(x <= tr[p].l && tr[p].r <= y) {
tr[p].sum += (tr[p].r - tr[p].l + 1) * k;
tr[p].add += k;
cout << "回 " << p << " sum " << tr[p].sum << " add " << tr[p].add << '\n';
return;
}
int m = tr[p].l + tr[p].r >> 1;
pushdown(p);
if(x <= m) update(lc, x, y, k);
if(y > m) update(rc, x, y, k);
pushup(p);
cout << "回 " << p << " sum " << tr[p].sum << '\n';
}
int query(int p, int x, int y) {
if(x <= tr[p].l && tr[p].r <= y) {
return tr[p].sum;
}
int m = tr[p].l + tr[p].r >> 1;
pushdown(p);
int sum = 0;
if(x <= m) sum += query(lc, x, y);
if(y > m) sum += query(rc, x, y);
return sum;
}
int main() {
cin >> n;
for(int i = 1; i <= n; i++) cin >> w[i];
build(1, 1, n);
update(1, 4, 9, 5);
cout << query(1, 4, 5);
return 0;
}
例题:
P3372 【模板】线段树 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这是一个区间进行修改,区间查询的模板题,直接使用带有懒标记的线段树模板,这里有一些细节注释在代码中
#include<bits/stdc++.h>
using namespace std;
#define lc p<<1 // 这里不要写成右移,因为这个调了好一会
#define rc p<<1|1
const int N = 1e5 + 10;
using ll = long long;
struct node {
int l, r;
ll sum;
int add;
}tr[4*N];
int n, m;
ll w[N];
void pushdown(int p) {
if(tr[p].add) {
tr[lc].sum += (tr[lc].r - tr[lc].l + 1) * tr[p].add;
tr[rc].sum += (tr[rc].r - tr[rc].l + 1) * tr[p].add;
tr[lc].add += tr[p].add;
tr[rc].add += tr[p].add;
tr[p].add = 0;
}
}
void pushup(int p) {
tr[p].sum = tr[lc].sum + tr[rc].sum;
}
void build(int p, int l, int r) {
tr[p] = {l, r, w[l], 0}; //带有懒标记,不要忘记0哦
if(l == r) return;
int m = tr[p].l + tr[p].r >> 1;
build(lc, l, m);
build(rc, m + 1, r);
pushup(p); //建树流程用叶子节点依次往上回溯得出结果
}
void update(int p, int x, int y, int k) {
if(x <= tr[p].l && tr[p].r <= y) {
tr[p].sum += (tr[p].r - tr[p].l + 1) * k;
tr[p].add += k;
return;
}
int m = tr[p].l + tr[p].r >> 1;
pushdown(p);
if(x <= m) update(lc, x, y, k);
if(y > m) update(rc, x, y, k);
pushup(p);
}
//题目注意要开long long
ll query(int p, int x, int y) {
if(x <= tr[p].l && tr[p].r <= y) {
return tr[p].sum;
}
int m = tr[p].l + tr[p].r >> 1;
pushdown(p);
ll ans = 0;
if(x <= m) ans += query(lc, x, y);
if(y > m) ans += query(rc, x, y); //这需要查询,没有修改,就没必要用pushup函数
return ans;
}
int main() {
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> w[i];
build(1, 1, n);
for(int i = 1; i <= m; i++) {
int op; cin >> op;
if(op == 1) {
int x, y, k; cin >> x >> y >> k;
update(1, x, y, k);
}
else {
int x, y; cin >> x >> y;
cout << query(1, x, y) << '\n';
}
}
return 0;
}
后序有题目会更新本文章
这里推荐如果是单点修改的区间查询,可以用树状数组实现,代码少不用建树。如果是区间修改&&区间查询的话,就需要用懒标记线段树。