线段树与懒标记

1040 字
5 分钟
线段树与懒标记

线段树与懒标记#

线段树适合处理区间问题,比如:

  • 区间求和
  • 区间最大值
  • 单点修改
  • 区间修改

当题目既有区间查询,又有区间修改时,通常需要懒标记。

1、线段树解决什么问题#

假设有数组:

1 2 3 4 5

需要频繁执行:

查询 [l, r] 的和
把 [l, r] 中每个数都加上 c

如果每次都暴力遍历区间,复杂度太高。线段树可以把这些操作做到 O(log n)。

2、线段树节点存什么#

常见节点结构:

struct Node {
int l, r;
long long sum;
long long add;
} tr[N * 4];

含义:

l, r:当前节点表示的区间
sum:当前区间的和
add:懒标记,表示这个区间整体还欠着多少增量没有下传

3、建树#

建树就是递归划分区间。

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

u 是当前节点编号,左儿子是 u << 1,右儿子是 u << 1 | 1。

4、pushup:用子节点更新父节点#

void pushup(int u) {
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

父节点的区间和等于左右子区间的和。

5、懒标记是什么#

如果某次修改完全覆盖当前区间,就不需要继续递归到叶子节点。

比如要让 [1, 100] 都加 5,当前节点刚好表示 [1, 100],直接更新:

sum += 区间长度 * 5
add += 5

这个 add 表示:以后如果需要访问子节点,再把这个修改传下去。

6、pushdown:下传懒标记#

void pushdown(int u) {
auto& root = tr[u];
auto& left = tr[u << 1];
auto& right = tr[u << 1 | 1];
if (root.add) {
left.add += root.add;
left.sum += (long long)(left.r - left.l + 1) * root.add;
right.add += root.add;
right.sum += (long long)(right.r - right.l + 1) * root.add;
root.add = 0;
}
}

只有当要继续往下访问子节点时,才需要 pushdown。

7、完整模板:区间加,区间求和#

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, m;
long long a[N];
struct Node {
int l, r;
long long sum;
long long add;
} tr[N * 4];
void pushup(int u) {
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void build(int u, int l, int r) {
tr[u] = {l, r, a[l], 0};
if (l == r) return;
int mid = (l + r) >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
pushup(u);
}
void pushdown(int u) {
auto& root = tr[u];
auto& left = tr[u << 1];
auto& right = tr[u << 1 | 1];
if (root.add) {
left.add += root.add;
left.sum += (long long)(left.r - left.l + 1) * root.add;
right.add += root.add;
right.sum += (long long)(right.r - right.l + 1) * root.add;
root.add = 0;
}
}
void modify(int u, int l, int r, int d) {
if (tr[u].l >= l && tr[u].r <= r) {
tr[u].sum += (long long)(tr[u].r - tr[u].l + 1) * d;
tr[u].add += d;
return;
}
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) modify(u << 1, l, r, d);
if (r > mid) modify(u << 1 | 1, l, r, d);
pushup(u);
}
long long query(int u, int l, int r) {
if (tr[u].l >= l && tr[u].r <= r) {
return tr[u].sum;
}
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
long long res = 0;
if (l <= mid) res += query(u << 1, l, r);
if (r > mid) res += query(u << 1 | 1, l, r);
return res;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
build(1, 1, n);
while (m--) {
char op;
int l, r, d;
cin >> op >> l >> r;
if (op == 'Q') {
cout << query(1, l, r) << endl;
} else {
cin >> d;
modify(1, l, r, d);
}
}
return 0;
}

8、容易写错的地方#

(1)数组要开 4 * N#

线段树节点数量一般开:

tr[N * 4]

不要只开 N。

(2)区间和建议用 long long#

区间加和区间求和很容易超过 int。

(3)查询和修改前要下传懒标记#

只要要访问子节点,就先:

pushdown(u);

否则子节点保存的值可能不是最新的。

原图笔记#

懒标记处理#

总结图#

总结#

线段树可以记成:

build 建树
pushup 子节点更新父节点
pushdown 懒标记下传
modify 区间修改
query 区间查询

懒标记的意义是:能不往下走就先不往下走,把修改暂存在当前节点,等真正需要访问子节点时再下传。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

线段树与懒标记
https://blog.tjcheng.cn/posts/migrated/old-91/
作者
TengJinCheng
发布于
2025-12-04
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
TengJinCheng
Hello, I'm Teng.
公告
欢迎来到TengJinCheng工作铺!
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
分类
标签
站点统计
文章
157
分类
7
标签
114
总字数
109,939
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.13.4
文章许可
CC BY-NC-SA 4.0

文章目录