OI-wiki 李超线段树(Li Chao Segment Tree)详解:一次函数区间最值的插入、查询与动态开点合并
发布时间:2026/9/12 18:17:10 作者:尧图编辑部 阅读量:1,286
详解:一次函数区间最值的插入、查询与动态开点合并)
OI-wiki 李超线段树Li Chao Segment Tree详解一次函数区间最值的插入、查询与动态开点合并【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki李超线段树Li Chao Segment Tree是 OI / ICPC 竞赛中用于「动态维护一次函数直线/线段在给定横坐标处的最值」的经典数据结构本指南以其在 OI-wiki 仓库中的 核心文档 为主体结合 仓库配套实现 展开。读完本文你将掌握李超线段树的引入动机、懒标记下传原理、O(log n)查询与O(log² n)区间插入实现以及基于动态开点的多树合并技巧。引入一个线段树难以直接维护的问题李超线段树解决的问题可以概括为在平面直角坐标系中动态维护若干线段支持「插入线段」与「在给定横坐标处查询最高交点」两类操作。以文档引入的经典例题 洛谷 P4097 [HEOI2013] Segment 为例题目要求维护两个操作强制在线在平面上加入一条线段记第 $i$ 条被插入的线段的标号为 $i$该线段两个端点分别为 $(x_0,y_0)$、$(x_1,y_1)$给定一个数 $k$询问与直线 $xk$ 相交的线段中交点纵坐标最大的线段的编号若有多条线段交点纵坐标并列最大输出编号最小的若不存在与给定直线相交的线段输出 $0$。数据规模为操作总数 $1 \le n \le 10^5$$1 \le k, x_0, x_1 \le 39989$$1 \le y_0, y_1 \le 10^9$。传统的线段树维护的是「点的信息」或「可合并的区间信息」而这里要求维护的是一族函数在某个横坐标处的取值极值函数之间是竞争关系而非可合并关系因此传统线段树难以很好地维护这类信息。这种情况下李超线段树便应运而生。问题转化从线段到定义域受限的一次函数我们可以把上述任务转化为如下等价的抽象操作加入一个一次函数 $f(x) kx b$定义域为 $[l, r]$即一条线段给定 $k$求所有定义域包含 $k$ 的一次函数中在 $x k$ 处取值最大的那个若有多个函数取值相同选编号最小的。注意垂直线段特判当线段垂直于 $x$ 轴时斜率计算会出现除以零的情况。文档给出的处理方式是假设线段两端点分别为 $(x, y_0)$ 和 $(x, y_1)$且 $y_0 y_1$则插入定义域为 $[x, x]$ 的一次函数 $f(x) 0 \cdot x y_1$。仓库实现 li-chao-tree_1.cpp 中的add函数正是这样处理的当x0 x1时令p[cnt].k 0, p[cnt].b max(y0, y1)。看到区间修改我们自然沿用线段树解决区间问题的常见思路给每个节点一个懒标记。每个节点 $i$ 的懒标记都是一条线段记为 $l_i$表示要用 $l_i$ 来更新该节点所表示的整个区间。核心过程懒标记的下传与只能影响一侧的性质现在需要插入一条线段 $f$考虑某个被新线段 $f$完整覆盖的线段树区间若该区间无标记直接打上用该线段更新的标记若该区间已有标记由于标记难以合并两条直线的较优者随横坐标变化而变化只能把标记下传。但子节点也有自己的标记同样可能产生冲突因此需要递归下传标记。关键性质在于按新线段 $f$ 取值是否大于原标记 $g$可以把当前区间分为两个子区间其中肯定有一个子区间被左区间或右区间完全包含。也就是说在两条线段中肯定有一条线段只可能成为左区间的答案或者只可能成为右区间的答案。我们用这条线段递归更新对应子树用另一条线段作为懒标记更新整个区间这保证了递归下传的复杂度——只有当一条线段只可能成为左或右区间的答案时它才会被下传所以不用担心漏掉某些线段。具体判断规则设当前区间的中点为 $m$拿新线段 $f$ 在中点处的值与原最优线段 $g$ 在中点处的值作比较若 $f$ 在中点更优则将 $f$ 与 $g$ 交换从而始终保证在中点处 $f$ 不如 $g$ 优的讨论前提。在 $f$ 不如 $g$ 优的前提下分三种情况若在左端点处 $f$ 更优$f$ 和 $g$ 必然在左半区间内产生了交点$f$ 只有在左区间才可能优于 $g$递归到左儿子下传若在右端点处 $f$ 更优$f$ 和 $g$ 必然在右半区间内产生了交点$f$ 只有在右区间才可能优于 $g$递归到右儿子下传若在左右端点处 $g$ 都更优$f$ 不可能成为答案无需继续下传。此外还有一类边界情况$f$ 和 $g$ 恰好交于中点。程序实现时可以将其归入中点处 $f$ 不如 $g$ 优的情况结果会朝 $f$ 更优的那个端点递归下传。最终将 $g$ 作为当前区间的懒标记保存。实现插入与下传的核心代码文档给出如下upd对线段完全覆盖到的区间进行修改实现。注意其中引入了浮点数比较函数cmp来处理精度误差constexpr double eps 1e-9; int cmp(double x, double y) { // 因为用到了浮点数所以会有精度误差 if (x - y eps) return 1; if (y - x eps) return -1; return 0; } //... void upd(int root, int cl, int cr, int u) { // 对线段完全覆盖到的区间进行修改 int v s[root], mid (cl cr) 1; int bmid cmp(calc(u, mid), calc(v, mid)); if (bmid 1 || (!bmid u v)) // 在此题中记得判线段编号 swap(u, v); int bl cmp(calc(u, cl), calc(v, cl)), br cmp(calc(u, cr), calc(v, cr)); if (bl 1 || (!bl u v)) upd(root 1, cl, mid, u); if (br 1 || (!br u v)) upd(root 1 | 1, mid 1, cr, u); // 上面两个 if 的条件最多只有一个成立这保证了李超树的时间复杂度 }实现细节值得展开说明s[root]存储当前节点懒标记线段的编号calc(u, d)计算编号为u的线段在横坐标d处的取值bmid 1 || (!bmid u v)中的后半部分用于处理中点处取值并列的情况——按题目要求选编号更小的线段这也是仓库源码 li-chao-tree_1.cpp 中原样保留的逻辑注释特别强调上面两个if的条件最多只有一个成立。这正是李超树复杂度的保证——每次下传只会进入一侧子树不会像普通区间修改那样递归两侧。插入线段时需要先定位所有被新线段完整覆盖的区间区间拆分再对每个区间调用updvoid update(int root, int cl, int cr, int l, int r, int u) { // 定位插入线段完全覆盖到的区间 if (l cl cr r) { upd(root, cl, cr, u); // 完全覆盖当前区间更新当前区间的标记 return; } int mid (cl cr) 1; if (l mid) update(root 1, cl, mid, l, r, u); // 递归拆分区间 if (mid r) update(root 1 | 1, mid 1, cr, l, r, u); }在 OI-wiki 的数据结构章节中李超线段树被收录于 线段树专题 的延伸条目之下与线段树其他扩展结构如线段树分治、权值线段树共同构成完整的区间维护工具集读者可将本文与 docs/ds/seg.md 对照阅读。一个重要澄清懒标记并不等价于区间中点处取值最大的线段文档特别提醒懒标记并不等价于在区间中点处取值最大的线段。如图所示加入黄色线段后只有红色节点的标记被更新而绿色节点的标记还未被改变但在第二、三、四个绿色区间的中点处显然是黄色线段取值最大。这说明懒标记的选择更多取决于哪条线段在该区间的优势区间更大而非单纯比较中点取值这也是下传规则要比较两个端点取值的原因。查询利用标记永久化思想查询时利用标记永久化思想在包含 $x$ 的所有线段树区间不超过 $O(\log n)$ 个的标记线段中逐一比较得出最终答案而不必把标记真正下传到叶子。pdi query(int root, int l, int r, int d) { // 查询 if (r d || d l) return {0, 0}; int mid (l r) 1; double res calc(s[root], d); if (l r) return {res, s[root]}; return pmax({res, s[root]}, pmax(query(root 1, l, mid, d), query(root 1 | 1, mid 1, r, d))); }其中pdi是pairdouble, int的别名pmax是对该 pair 定义的先比纵坐标、纵坐标相同时取编号较小者的取最大值函数见仓库源码 li-chao-tree_1.cpp。查询路径上的每个节点只需 $O(1)$ 比较因此单次查询为 $O(\log n)$。复杂度分析查询沿根到叶子的一条路径比较沿途每个节点的懒标记时间复杂度显然为 $O(\log n)$插入需要将原线段拆分到 $O(\log n)$ 个完整覆盖的区间中对于每个区间又需要花费 $O(\log n)$ 的时间递归下传因此插入过程的时间复杂度为 $O(\log^2 n)$。完整参考代码文档以 HEOI2013 Segment 的完整 AC 代码作为配套实现docs/ds/code/li-chao-tree/li-chao-tree_1.cpp仓库源码与文档叙述完全对应可直接编译运行验证。代码要点如下坐标离散范围由MOD1 39989与MOD2 1000000000定义与题目数据范围一致线段以struct line { double k, b; }存储于数组p[]s[]为线段树的懒标记记录线段编号数组大小按 $4 \times 10^4$ 级别节点预估为160005主程序在读入后对输入进行(x lastans - 1) % MOD 1的强制在线解码并通过x0 x1时交换保证x0 x1随后调用add与update查询结果query(1, 1, MOD1, x).second即为满足要求的线段编号同时作为lastans参与后续输入的在线解码。扩展多棵李超线段树的合并动态开点除了单棵树的插入与查询李超线段树还支持类似普通线段树的合并操作。合并常用于树上统计、DSU on tree、斜率 DP 优化等场景。文档给出如下定义将两个李超线段树节点 $u, v$ 合并并以 $u$ 作为新的根如果 $v$ 为空结束过程如果 $u$ 为空将 $v$ 复制给 $u$将 $v$ 对应线段插入到以 $u$ 为根的子树递归将 $u, v$ 的左右子树对应合并。由于涉及多棵树的合并实现需采用动态开点void upd(int root, int cl, int cr, int u) { // 涉及多棵李超线段树合并使用动态开点 static int idx 0; if (!root) { s[root idx] u; return; } int v s[root], mid (cl cr) 1; int bmid cmp(calc(u, mid), calc(v, mid)); if (bmid 1 || (!bmid u v)) swap(u, v); int bl cmp(calc(u, cl), calc(v, cl)), br cmp(calc(u, cr), calc(v, cr)); if (bl 1 || (!bl u v)) upd(ls[root], cl, mid, u); if (br 1 || (!br u v)) upd(rs[root], mid 1, cr, u); } int merge(int u, int v, int l, int r) { if (!u || !v) { return u v; } if (l r) { int b cmp(calc(s[v], l), calc(s[u], l)); if (b 1 || (!b s[v] s[u])) return v; return u; } upd(u, l, r, s[v]); int mid (l r) 1; ls[u] merge(ls[u], ls[v], l, mid); rs[u] merge(rs[u], rs[v], mid 1, r); return u; }其中merge中if (!u || !v) return u v;利用了空节点编号为 0的动态开点约定一次返回非空子树是线段树合并的经典写法。复杂度若合并若干李超线段树涉及的总点数为 $n$则合并过程复杂度为 $O(n \log n)$。原因是对于任意线段在树上对应的节点每次涉及移动它时要么使其深度 $1$要么直接从树上删除两个操作的代价都是 $O(1)$ 的而每个节点深度至多为 $O(\log n)$于是总复杂度即为 $O(n \log n)$。练习题目以下是文档收录的经典习题可用于检验对插入、查询与合并的掌握程度「JSOI2008」Blue Mary 开公司斜率优化与李超树结合的入门题「CodeChef」TSUM2 Sum on Tree树上问题与李超树合并「USACO13MAR」Hill Walk G线段/直线插入的综合应用「CF932F」Escape Through Leaf树上 DP 与李超树合并的经典组合建议先独立完成 P4097 [HEOI2013] Segment 的编码再逐步挑战上述题目重点体会编号最小的并列处理与动态开点合并的边界条件。【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考