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

【树形数据结构】李超线段树 (Li-Chao Tree)

李超线段树是一种用于维护动态插入线段(或直线),并支持 在指定点查询所有线段在该点的最大值(或最小值) 的数据结构。

1. 问题背景

想象一下,你有以下需求:

  • 在线操作:不断地向一个集合中添加新的线段(或直线)。
  • 单点查询:给定一个横坐标 x,需要快速查询所有已插入的线段在 x 处的函数值的最大值(或最小值)。

例如,在计算几何、动态规划优化(如斜率优化)中,我们常常需要处理这类问题。朴素的做法是每次查询都遍历所有线段,时间复杂度为 O(n),效率低下。

李超线段树可以在 O(log C) 的时间内完成单次插入和查询操作,其中 C 是坐标范围的大小(或离散化后的点数)。

2. 核心思想

李超线段树的核心思想是:在每个线段树节点上,只维护一个“优势”线段。

这里的“优势”指的是:在当前节点所代表的区间中点处,该线段的函数值是最大的(或最小的)

  • 为什么是中点? 选择中点是为了保证树的平衡性和操作的正确性。通过比较中点处的函数值,可以决定新插入的线段是否应该“覆盖”当前节点存储的线段,或者递归到子区间。

3. 数据结构定义

  • 线段/直线:通常表示为 y = k * x + b。我们用一个结构体 Line 来存储 kb
  • 线段树:是一棵完全二叉树,通常建立在横坐标上。
    • 叶子节点代表一个具体的横坐标点。
    • 内部节点代表一个区间 [l, r]
  • 节点信息:每个节点 u 存储一条线段 tree[u],这条线段是在区间 [l, r] 的中点 mid = (l + r) / 2 处具有最大(或最小)函数值的线段。

4. 操作详解

4.1 插入操作 (Insert)

目标:将一条新线段 L 插入到线段树中。

过程(递归进行):

  1. 初始化:从根节点开始,当前处理的节点 u 对应区间 [l, r]
  2. 计算中点mid = (l + r) / 2
  3. 比较中点函数值
    • 计算当前节点存储的线段 tree[u]mid 处的值:val_old = tree[u].f(mid)
    • 计算新线段 Lmid 处的值:val_new = L.f(mid)
  4. 决策
    • 如果 val_new > val_old(假设求最大值),说明新线段 L 在中点处更优,那么:
      • tree[u] 替换为 L
      • 将原来的 tree[u] 作为新的待插入线段,递归地插入到左子树右子树中。
    • 否则(val_new <= val_old),说明当前节点的线段在中点处更优或相等,那么:
      • 将新线段 L 递归地插入到左子树右子树中。
  5. 递归方向
    • 如何决定递归到左子树还是右子树?
    • 关键在于比较两条线段在区间端点的函数值。
    • 比较左端点 l 处的函数值
      • 如果 L.f(l) > tree[u].f(l),说明新线段在左半区间可能更优,递归到左子树。
      • 否则,递归到右子树。
    • (注意:也可以比较右端点 r,但比较左端点更常见。选择哪个端点不影响正确性,但可能影响常数。)

为什么这样递归?
这个决策基于一个重要的几何性质:两条直线最多相交一次

  • 如果两条线在中点 mid 处,新线段更优,但在左端点 l 处旧线段更优,说明两条线在区间 [l, mid] 内相交。因此,旧线段在左半区间可能仍有优势,需要递归到左子树检查。
  • 同理,如果在右端点 r 处旧线段更优,则递归到右子树。

4.2 查询操作 (Query)

目标:查询在横坐标 x 处所有线段的最大函数值。

过程:

  1. 从根节点开始,沿着线段树向下遍历。
  2. 对于经过的每一个节点 u
    • 计算该节点存储的线段 tree[u]x 处的函数值。
    • 用这个值更新全局最大值 ans
  3. 根据 x 的大小,决定进入左子树还是右子树:
    • 如果 x <= mid,进入左子树。
    • 如果 x > mid,进入右子树。
  4. 当到达叶子节点时,返回 ans

为什么这样查询?
因为任何一条线段 L,只要它曾经在某个包含 x 的区间中被存储在某个节点上,那么在查询 x 时,我们一定会经过那个节点,并计算 Lx 处的值。由于我们维护的是“中点优势”,即使 L 不是最终在 x 处最优的线段,它也可能在某个祖先节点上被记录过。通过遍历所有包含 x 的区间对应的节点,我们保证了不会遗漏任何可能在 x 处取得最大值的线段。

5. 复杂度分析

  • 时间复杂度
    • 插入:每次插入最多递归树的高度次。树的高度为 O(log C),其中 C 是坐标范围。因此,单次插入时间复杂度为 O(log C)
    • 查询:查询需要从根到叶子的路径,路径长度为树的高度。因此,单次查询时间复杂度为 O(log C)
  • 空间复杂度:线段树需要 O© 的空间来存储所有节点。如果坐标范围很大,通常需要离散化

6. 坐标离散化

当横坐标范围 C 非常大(例如 10^9)时,直接开 O© 的数组是不现实的。

解决方案:离散化

  1. 收集所有可能用到的横坐标值(包括插入线段的定义域和查询点)。
  2. 对这些值进行排序并去重。
  3. 建立一个映射,将原始坐标映射到 [1, M] 的整数,其中 M 是去重后的坐标数量。
  4. 在离散化后的坐标上建立李超线段树。

离散化后,树的高度变为 O(log M),插入和查询的时间复杂度也变为 O(log M)。

7. C++ 实现

以下是一个完整的、可运行的 C++ 实现,支持离散化,求最大值。

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;// 定义线段/直线 y = k*x + b
struct Line {long long k, b;Line() : k(0), b(0) {}Line(long long k, long long b) : k(k), b(b) {}// 计算在 x 处的函数值long long f(long long x) const {return k * x + b;}
};// 李超线段树类
class LiChaoTree {
private:vector<Line> tree; // 线段树数组,每个节点存储一条线段vector<long long> xs; // 离散化后的横坐标数组int n; // 离散化后的坐标数量// 比较函数:判断 line1 在 x 处的值是否大于 line2// 用于维护最大值bool better(const Line& line1, const Line& line2, long long x) {long long val1 = line1.f(x);long long val2 = line2.f(x);// 如果值相等,优先选择新插入的线段(避免死循环)if (val1 != val2) return val1 > val2;return line1.k > line2.k; // 任意规则打破平局}// 递归插入线段// u: 当前节点编号// l, r: 当前节点代表的区间 [l, r] (在 xs 数组中的索引)// L: 要插入的线段void insert(int u, int l, int r, Line L) {if (l == r) {// 叶子节点,直接比较并更新if (better(L, tree[u], xs[l])) {tree[u] = L;}return;}int mid = (l + r) / 2;// 计算中点坐标long long x_mid = xs[mid];// 判断是否需要交换bool better_at_mid = better(L, tree[u], x_mid);if (better_at_mid) {swap(tree[u], L); // 将更优的线段留在当前节点}// 计算左端点坐标long long x_left = xs[l];// 判断递归方向:比较在左端点的函数值if (better(L, tree[u], x_left)) {// 新线段在左端点更优,递归到左子树insert(u * 2, l, mid, L);} else {// 否则递归到右子树insert(u * 2 + 1, mid + 1, r, L);}}// 查询在坐标 x 处的最大函数值// u: 当前节点编号// l, r: 当前节点区间// x: 查询的横坐标long long query(int u, int l, int r, long long x) {if (l == r) {return tree[u].f(x);}int mid = (l + r) / 2;long long ans = tree[u].f(x); // 当前节点线段的贡献if (x <= xs[mid]) {// 查询点在左半区间ans = max(ans, query(u * 2, l, mid, x));} else {// 查询点在右半区间ans = max(ans, query(u * 2 + 1, mid + 1, r, x));}return ans;}public:// 构造函数:接收离散化的坐标数组LiChaoTree(vector<long long> coordinates) {// 去重并排序sort(coordinates.begin(), coordinates.end());coordinates.erase(unique(coordinates.begin(), coordinates.end()), coordinates.end());xs = coordinates;n = xs.size();// 初始化线段树,大小为 4*ntree.resize(4 * n, Line(0, LLONG_MIN)); // 初始线段为负无穷,确保任何线段都更优}// 插入一条线段void insert(Line L) {insert(1, 0, n - 1, L);}// 查询在 x 处的最大值long long query(long long x) {// 找到 x 在离散化数组中的位置(二分查找)auto it = lower_bound(xs.begin(), xs.end(), x);// 如果 x 不在离散化数组中,找到第一个 >= x 的位置// 但查询仍然有效,因为树的结构支持任意 xreturn query(1, 0, n - 1, x);}
};// ===================== 使用示例 =====================int main() {// 收集所有可能的横坐标(包括查询点和线段定义域)vector<long long> coords = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 示例坐标范围 1 到 10// 实际应用中,coords 应包含所有查询点和线段端点// 创建李超线段树LiChaoTree lct(coords);// 插入几条线段lct.insert(Line(1, 0));  // y = xlct.insert(Line(0, 5));  // y = 5lct.insert(Line(-1, 10)); // y = -x + 10lct.insert(Line(2, -5));  // y = 2x - 5// 进行查询cout << "Query at x=1: " << lct.query(1) << endl; // 应该是 max(1, 5, 9, -3) = 9cout << "Query at x=3: " << lct.query(3) << endl; // 应该是 max(3, 5, 7, 1) = 7cout << "Query at x=5: " << lct.query(5) << endl; // 应该是 max(5, 5, 5, 5) = 5cout << "Query at x=7: " << lct.query(7) << endl; // 应该是 max(7, 5, 3, 9) = 9return 0;
}

8. 代码说明

  • Line 结构体:表示一条直线 y = kx + b,包含计算函数值的 f 方法。
  • better 函数:用于比较两条线段在某个 x 处的函数值,决定哪条更优(求最大值)。这里加入了 k 的比较来打破平局,防止无限递归。
  • insert 函数:递归插入。核心是中点比较和交换,然后根据左端点比较决定递归方向。
  • query 函数:递归查询,遍历所有包含查询点的节点,取最大值。
  • 构造函数:接收坐标数组,进行排序、去重,并初始化线段树数组。初始线段设置为 k=0, b=LLONG_MIN,代表负无穷,确保任何有效线段插入时都能被接受。
  • query 公共接口:使用 lower_bound 查找坐标,但实际查询时传入原始 x 值,因为 f(x) 计算不依赖于离散化索引。

9. 注意事项

  • 精度问题:如果使用浮点数,需要注意精度误差。通常建议使用整数或高精度浮点数。
  • 求最小值:只需将 better 函数的比较逻辑改为 <,并将初始线段的 b 设为 LLONG_MAX
  • 线段 vs 直线:上述实现处理的是无限长的直线。如果需要处理有定义域的线段,可以在 f 函数中加入范围检查,或者使用更复杂的变体。
  • 常数优化:可以尝试不同的递归方向判断(如比较右端点),有时能获得更好的性能。

10. 总结

李超线段树是一种巧妙的数据结构,利用“中点优势”和直线相交性质,高效地解决了动态插入线段并单点查询最值的问题。通过离散化,它可以处理大范围的坐标。理解其核心思想——“在每个区间维护中点最优的线段”——是掌握它的关键。

http://www.dtcms.com/a/359212.html

相关文章:

  • 【深度学习新浪潮】有没有什么方法可以将照片变成线描稿,比如日式漫画的那种?
  • 嵌入式学习日记(38)HTTP
  • Ansible主机模式与文件导入技巧
  • 开发环境全面配置指南:语言环境与数据库工具
  • 【面试场景题】订单超时自动取消功能如何设计
  • 【机器学习入门】3.3 FP树算法——高效挖掘频繁项集的“树状神器”
  • 11 C 语言 sizeof 与指针实战指南:一维 / 二维数组计算注意事项 + 笔试真题解析 + sizeof strlen 对比
  • 谈谈线程的中断退出
  • nginx(自写)
  • [Windows] 剪映国际版CapCut 6.7.0 视频编辑处理,免费使用素材和滤镜
  • 倾斜摄影是选择RGB图像还是多光谱影响进行操作?
  • RestTemplate工具类用法总结
  • AI融合高等教育:从通识到专业 - 学科+AI人才培养白皮书(下)
  • 最长递增子序列(LIS)的 DFS 解法详解与实现
  • Session
  • PB-重装系统后,重新注册ole控件,pb中窗口控件失效的问题。
  • 2025年06月GESPC++三级真题解析(含视频)
  • 【小宁学习日记5 PCB】电路定理
  • Unity游戏打包——GooglePlay自动传包
  • DFS 回溯 【各种题型+对应LeetCode习题练习】
  • 【多项式】快速莫比乌斯变换(FMT)
  • CCS自定义函数.h与.c问题解决办法
  • Android15适配16kb
  • 计算机毕设项目 基于Python与机器学习的B站视频热度分析与预测系统 基于随机森林算法的B站视频内容热度预测系统
  • Robolectric拿到当前的Activity
  • 轻量化模型-知识蒸馏1
  • Wheat Gene ID Convert Tool 小麦中国春不同参考基因组GeneID转换在线工具
  • 2025年外贸服装跟单管理软件TOP3推荐榜单
  • 手动安装的node到nvm吧版本管理的过程。
  • 基于Docker部署的Teable应用