当前位置: 首页 > news >正文

线段树 (Segment Tree)

线段树 (Segment Tree) 相较于树状数组,它更灵活,能解决更复杂的问题,当然代码也更长一些。

我们将从基础的单点更新、区间查询开始,然后讲解其最强大的特性——区间更新(懒惰标记)。


一、 核心思想:分治 (Divide and Conquer)

线段树的核心思想是分治。它将一个大的区间 [L, R] 不断地二分成两个子区间 [L, M][M+1, R](其中 M = (L+R)/2),直到每个区间只包含一个元素。然后,通过合并子区间的信息来得到父区间的信息。

  • 它是一棵完全二叉树
  • 每个节点代表原始数组的一个区间 [l, r]
  • 根节点代表整个数组的区间,例如 [1, N]
  • 叶子节点代表只包含单个元素的区间,例如 [i, i]
  • 一个代表区间 [l, r] 的非叶子节点,其左子节点代表 [l, mid],右子节点代表 [mid+1, r],其中 mid = (l+r)/2

这个结构使得我们可以在 O(log N) 的时间内,将任意一个查询区间 [ql, qr] 分解成树上 O(log N) 个节点所代表的区间的组合。

二、 树的存储

和堆一样,我们通常用一个数组来存储这棵树。如果根节点存储在数组索引 p 处:

  • 其左子节点在 2 * p (或 p << 1)
  • 其右子节点在 2 * p + 1 (或 p << 1 | 1)

为了保证不越界,这个数组的大小通常需要开到 4 * N


三、 基础线段树:单点更新,区间查询

我们以最经典的区间求和为例。

1. 结构定义
const int MAXN = 100005;
int N;
long long a[MAXN];      // 原始数组
long long tree[MAXN * 4]; // 线段树数组
2. push_up 操作

这是一个辅助函数,用来通过子节点的信息更新父节点。对于区间求和,父节点的值就是左右子节点的值相加。

void push_up(int p) {tree[p] = tree[p * 2] + tree[p * 2 + 1];
}
3. build 操作 (建树)

递归地将原始数组 a 的信息构建到 tree 数组中。

  • 参数: p (当前节点在 tree 数组中的索引), l, r (当前节点代表的区间)。
  • 逻辑:
    • 如果 l == r,说明是叶子节点,直接存入 a[l] 的值。
    • 否则,递归地为左子树 [l, mid] 和右子树 [mid+1, r] 建树,然后用 push_up 合并信息。
void build(int p, int l, int r) {if (l == r) {tree[p] = a[l];return;}int mid = l + (r - l) / 2;build(p * 2, l, mid);build(p * 2 + 1, mid + 1, r);push_up(p);
}
4. update 操作 (单点更新)

a[idx] 的值修改为 val

  • 参数: p, l, r (同上), idx (要修改的原始数组下标), val (新的值)。
  • 逻辑:
    • 从根节点开始,递归地向下查找。
    • 如果 l == r,说明找到了代表 a[idx] 的叶子节点,更新它的值。
    • 否则,判断 idx 在左子区间还是右子区间,并递归下去。
    • 关键:在递归返回后,调用 push_up 来更新路径上所有祖先节点的值。
void update(int p, int l, int r, int idx, int val) {if (l == r) {tree[p] = val;return;}int mid = l + (r - l) / 2;if (idx <= mid) {update(p * 2, l, mid, idx, val);} else {update(p * 2 + 1, mid + 1, r, idx, val);}push_up(p);
}
5. query 操作 (区间查询)

查询区间 [ql, qr] 的和。

  • 参数: p, l, r (同上), ql, qr (要查询的区间)。
  • 逻辑:
    1. 完全覆盖: 如果当前节点代表的区间 [l, r] 被查询区间 [ql, qr] 完全包含,直接返回当前节点的值 tree[p]。这是效率的关键。
    2. 部分覆盖: 如果当前区间与查询区间有交集,但不是完全覆盖。
      • 递归地到左子树查询 [ql, qr][l, mid] 的交集部分。
      • 递归地到右子树查询 [ql, qr][mid+1, r] 的交集部分。
      • 将两边的结果合并(相加)。
    3. 无覆盖: 如果当前区间与查询区间没有交集,返回一个不影响结果的单位元(对于求和是0,求最大值是-∞)。
long long query(int p, int l, int r, int ql, int qr) {// Case 1: [l, r] is fully inside [ql, qr]if (ql <= l && r <= qr) {return tree[p];}int mid = l + (r - l) / 2;long long sum = 0;// Recurse into left child if there is overlapif (ql <= mid) {sum += query(p * 2, l, mid, ql, qr);}// Recurse into right child if there is overlapif (qr > mid) {sum += query(p * 2 + 1, mid + 1, r, ql, qr);}return sum;
}

四、 进阶线段树:懒惰标记 (Lazy Propagation)

这是线段树最强大的地方,它能高效处理区间更新问题(例如,给区间 [ul, ur] 里的每个数都加上 delta)。

如果对区间 [ul, ur] 的每个数都进行一次单点更新,复杂度会退化到 O(N log N),无法接受。

懒惰标记的思想:当一次更新操作覆盖了一个节点所代表的整个区间时,我们不再继续向下更新,而是在这个节点上打一个“标记”(tag),表示“我的所有子孙节点都欠着一个更新操作”。然后我们立刻更新当前节点的值,然后就返回。

这个标记只有在将来有查询或更新操作需要经过这个节点并进入其子树时,才会被“下放”(push_down)给它的直接子节点。

1. 新增结构
// ... tree 数组等不变 ...
long long lazy[MAXN * 4]; // 懒惰标记数组
2. push_down 操作

这是懒惰标记的核心。在访问一个节点的子节点之前,必须先执行 push_down

  • 参数: p (当前节点索引), l, r (当前节点代表的区间)。
  • 逻辑:
    • 检查当前节点 p 是否有懒惰标记 lazy[p]
    • 如果有,将这个标记的效果应用到左右两个子节点的值上。
    • 将标记传递给左右两个子节点的懒惰标记数组。
    • 清除当前节点的懒惰标记。
void push_down(int p, int l, int r) {if (lazy[p] != 0) { // 如果有标记int mid = l + (r - l) / 2;// 更新左子节点的值和懒惰标记tree[p * 2] += lazy[p] * (mid - l + 1);lazy[p * 2] += lazy[p];// 更新右子节点的值和懒惰标记tree[p * 2 + 1] += lazy[p] * (r - (mid + 1) + 1);lazy[p * 2 + 1] += lazy[p];// 清除当前节点的标记lazy[p] = 0;}
}
3. 修改后的 update (区间更新)
void update(int p, int l, int r, int ul, int ur, int delta) {// Case 1: [l, r] is fully inside the update range [ul, ur]if (ul <= l && r <= ur) {tree[p] += (long long)delta * (r - l + 1); // 更新当前节点的值lazy[p] += delta; // 打上懒惰标记return;}// Before recursing, push down the lazy tagpush_down(p, l, r);int mid = l + (r - l) / 2;if (ul <= mid) {update(p * 2, l, mid, ul, ur, delta);}if (ur > mid) {update(p * 2 + 1, mid + 1, r, ul, ur, delta);}// After updating children, update selfpush_up(p);
}
4. 修改后的 query

查询操作也需要 push_down,以确保在访问子节点前,它们的值是最新的。

long long query(int p, int l, int r, int ql, int qr) {if (ql <= l && r <= qr) {return tree[p];}// Before recursing, push down the lazy tagpush_down(p, l, r);int mid = l + (r - l) / 2;long long sum = 0;if (ql <= mid) {sum += query(p * 2, l, mid, ql, qr);}if (qr > mid) {sum += query(p * 2 + 1, mid + 1, r, ql, qr);}return sum;
}

五、 总结与对比

特性树状数组 (BIT)线段树
解决问题单点更新, 前缀区间查询单点/区间更新, 任意区间查询
功能较局限,但可扩展(如差分)非常灵活,支持各种区间操作(求和、最值、GCD等)
复杂度O(log N)O(log N)
常数,速度非常快较大,比BIT慢2-4倍
代码长度,易于实现较长,容易出错
空间复杂度O(N)O(4N)

何时使用?

  • 如果问题只需要单点更新和区间(或前缀)求和/异或,并且追求极致的速度和简洁的代码,首选树状数组
  • 如果问题涉及区间更新,或者需要查询区间最大/最小值等不满足简单前缀和性质的操作,必须使用线段树
http://www.dtcms.com/a/355467.html

相关文章:

  • 理解AI智能体:智能体记忆
  • day04-kubernetes(k8s)
  • 微动开关-电竞鼠标核心!5000万次寿命微动开关评测
  • windows PowerToys之无界鼠标:一套键鼠控制多台设备
  • 【详细教程】如何将SQLBot的MCP服务集成到n8n
  • Linux_详解线程池
  • 【mysql】SQL HAVING子句详解:分组过滤的正确姿势
  • SystemVerilog学习【六】功能覆盖率详解
  • OpenCV 4.9+ 进阶技巧与优化
  • Shell编程(一)
  • 流线型(2型)通风排烟天窗/TPC-A2
  • LoRA modules_to_save解析及卸载适配器(62)
  • C语言学习-24-柔性数组
  • 科技守护古树魂:古树制茶行业的数字化转型之路
  • TikTok 在电脑也能养号?网页端多号养号教程
  • 损失函数,及其优化方法
  • [Ai Agent] 从零开始搭建第一个智能体
  • 麒麟操作系统挂载NAS服务器
  • 搜维尔科技核心产品矩阵涵盖从硬件感知到软件渲染的全产品供应链
  • 12KM无人机高清图传通信模组——打造未来空中通信新高度
  • hintcon2025 Pholyglot!
  • 辅助驾驶出海、具身智能落地,稀缺的3D数据从哪里来?
  • kubernetes-ubuntu24.04操作系统部署k8s集群
  • 吃透 OpenHarmony 资源调度:核心机制、调度策略与多设备协同实战
  • Linux(二) | 文件基本属性与链接扩展
  • ManusAI:多语言手写识别的技术革命
  • SLF4J和LogBack
  • Linux 命令使用案例:文件和目录管理
  • 从0开始学习Java+AI知识点总结-27.web实战(Maven高级)
  • Python Imaging Library (PIL) 全面指南:PIL基础入门-图像滤波与处理技术