【学习笔记】李超线段树
前言
请问一下,\(\text{Link-Cut Tree}\) 与李超树不是同一个东西吗?它们好像都是 \(\text{LCT}\),那应该没区别吧(迷雾)。
当然今天讲的不是大名鼎鼎的 \(\text{Link-Cut Tree}\),不过名气与它也差不多。OK,废话不多说,开始进入我们的主题!
原理
李超线段树的定义:李超线段树是一种用于维护平面直角坐标系内线段关系的数据结构。
李超线段树的经典应用:给定一个平面直角坐标系,支持动态插入一条线段,对于每一个询问,给一条竖线,问这条竖线与所有线段的最高的交点(即 \(y\) 值最大)。题目传送门。

如上图,两条蓝色的直线就是两个询问,那么答案分别就是 \(A\) 点和 \(B\) 点。
这里我们要引入一个概念:最优势线段。最优势线段的定义:当前区间中点处最高的线段。
李超线段树维护的是有可能成为当前区间的最优势线段。某个节点的最优势线段在这个节点到根节点路径上所有最优势线段之中。
为何要维护这个东西呢?因为如果你不维护它的话,那么你每加入一条线段就要把整个区间跑一遍,时间复杂度飞到 \(O(n^2)\)。
而如果像我们这样子维护的话,我们就不用每次加入线段都遍历整个区间,而是可以在一个可以替换的区间停下。
插入线段
我们可以找到以下几种情况:
- 该线段与当前线段树枚举到的区间不相交,此时直接返回即可。
- 该线段部分覆盖到了当前线段树枚举到的区间,递归到左右子区间继续处理,使得最后的线段能完全覆盖线段树上的区间。
- 该线段完全覆盖到了当前线段树枚举到的区间,则接着分讨:
- 新线段在两端都更优
若该线段在两个端点处值均比当前区间的最优势线段更大,则将当前区间的最优势线段设为该线段,然后返回(此时旧的最优势线段已经无用了)。
- 新线段在两端都更劣
若该线段在两个端点处的值均比当前区间的最优势线段更小,则直接返回(此时新加的线段已经无用了)。
- 新旧线段交叉(一个端点新线段大,另一个端点旧线段大)
由于我们的线段树维护的是中点的最优势线段,所以:
- 首先判断新加入的线段和原先区间的最优势线段在中点位置的大小,并更新该区间的最优势线段。
- 由于这两条线段不能互相替代,我们不能直接将一条线段舍弃,而是要将未被记录的线段向两边递归。
- 但因为在中点处未被记录的线段更小,所以在某一侧它完全小于被记录的线段,因此只需要向另一侧递归即可。
说明:被记录的线段指存储在该区间的最优势线段,未被记录的线段指另一条线段。
这样我们动态插入线段就搞定了,可以参照下面代码理解:
bool cmp(int i, int j, int x) {
if (seg[i].k * x + seg[i].b - (seg[j].k * x + seg[j].b) > eps) return 1;
if (seg[j].k * x + seg[j].b - (seg[i].k * x + seg[i].b) > eps) return 0;
return i < j;
}
void change(int &x, int l, int r, int sl, int sr, int now) {
if (r < sl || sr < l) return; // 完全无交集
if (!x) x = ++Tcnt; // 动态开点
if (sl <= l && r <= sr) { // 完全覆盖
if (cmp(now, tree[x].id, l) && cmp(now, tree[x].id, r)) {
tree[x].id = now; // 更新
return;
}
if (cmp(tree[x].id, now, l) && cmp(tree[x].id, now, r))
return; // 什么用都没有,直接返回
int mid = (l + r) >> 1;
// 原始最优势线段与新加入线段不能相互代替,因此要不断递归
if (cmp(now, tree[x].id, mid)) swap(now, tree[x].id); // 让中点较小的作为 now
if (cmp(now, tree[x].id, l))
change(tree[x].ls, l, mid, sl, sr, now); // 左端点更大,有用
if (cmp(now, tree[x].id, r))
change(tree[x].rs, mid + 1, r, sl, sr, now); // 右端点更大,有用
} else { // 部分覆盖,继续递归至完全覆盖
int mid = (l + r) >> 1;
change(tree[x].ls, l, mid, sl, sr, now);
change(tree[x].rs, mid + 1, r, sl, sr, now);
}
}
查询就更简单了,直接往下找到该区间,并且将路径上所有区间的最优势线段都拿出来比较一下,可以参照下面代码理解。
void solve(int x, int l, int r, int s) {
if (!x || r < s || s < l) return ; // 遇到不合法的线段(即不包含 s 的线段)要返回
if (cmp(tree[x].id, ansid, s)) ansid = tree[x].id; // 往这一条路上一直找答案
if (l == r) return ;
int mid = (l + r) >> 1;
solve(tree[x].ls, l, mid, s), solve(tree[x].rs, mid + 1, r, s);
}
到这里,李超线段树就完成了!!!
考虑一下时间复杂度,我们知道线段会在线段树上分成 \(\log_2 n\) 个区间,而看到我们插入操作,分成的每个区间不会再分裂,而只会往一边更新,最多更新 \(\log_2 n\) 层,所以时间复杂度是 \(O({\log^2}_n)\)。
然后我们就可以来看这道板题,没什么好说的,直接上代码。
唯一的坑点就是精度问题。
#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-8;
const int N = 1e5 + 10;
const int mod1 = 39989;
const int mod2 = 1e9;
int T, Lastans, n = 39989, m, Tcnt, Rt, ansid;
struct Line { double k, b; } seg[N * 4];
struct Tree { int ls, rs, id; } tree[N * 4];
bool cmp(int i, int j, int x) {
if (seg[i].k * x + seg[i].b - (seg[j].k * x + seg[j].b) > eps) return 1;
if (seg[j].k * x + seg[j].b - (seg[i].k * x + seg[i].b) > eps) return 0;
return i < j;
}
void change(int &x, int l, int r, int sl, int sr, int now) {
if (r < sl || sr < l) return ; // 完全无连接
if (!x) x = ++Tcnt; // 动态开点
if (sl <= l && r <= sr) { // 完全覆盖
// 整个 [l, r] 都覆盖了
if (cmp(now, tree[x].id, l) && cmp(now, tree[x].id, r)) {
tree[x].id = now; // 更新
return ;
}
// 什么用都没有,直接返回
if (cmp(tree[x].id, now, l) && cmp(tree[x].id, now, r))
return ;
int mid = (l + r) >> 1;
// 这里原始最优势线段与新加入线段不能相互代替,因此要不断递归,发挥这两条线段的作用
if (cmp(now, tree[x].id, mid)) swap(now, tree[x].id); // 让中点较小的那个点作为 now,使接下来的更新更方便
if (cmp(now, tree[x].id, l)) change(tree[x].ls, l, mid, sl, sr, now); // 尽管中点更小,但是左端点更大。有用,因此递归
if (cmp(now, tree[x].id, r)) change(tree[x].rs, mid + 1, r, sl, sr, now); // 尽管中点更小,但是右端点更大。有用,因此递归
} else { // 部分覆盖(继续递归至完全覆盖)
int mid = (l + r) >> 1;
change(tree[x].ls, l, mid, sl, sr, now), change(tree[x].rs, mid + 1, r, sl, sr, now);
}
}
void solve(int x, int l, int r, int s) {
if (!x || r < s || s < l) return ; // 遇到不合法的线段(即不包含 s 的线段)要返回
if (cmp(tree[x].id, ansid, s)) ansid = tree[x].id; // 往这一条路上一直找答案
if (l == r) return ;
int mid = (l + r) >> 1;
solve(tree[x].ls, l, mid, s), solve(tree[x].rs, mid + 1, r, s);
}
int main() {
cin >> T;
while (T--) {
int id, x, sx, sy, ex, ey;
cin >> id;
if (id == 0) {
ansid = 0;
cin >> x, x = (x + Lastans - 1) % n + 1;
solve(1, 1, n, x), Lastans = ansid;
cout << ansid << endl;
} else {
cin >> sx >> sy >> ex >> ey;
sx = (sx + Lastans - 1) % mod1 + 1, sy = (sy + Lastans - 1) % mod2 + 1;
ex = (ex + Lastans - 1) % mod1 + 1, ey = (ey + Lastans - 1) % mod2 + 1;
if (ex < sx) swap(sx, ex), swap(sy, ey); // 不写这个的就是 bangbangt
if (ex != sx) m++, seg[m].k = 1.0 * (ey - sy) / (ex - sx), seg[m].b = 1.0 * sy - seg[m].k * sx;
else m++, seg[m].k = 0, seg[m].b = max(sy, ey);
change(Rt, 1, n, sx, ex, m);
}
}
return 0;
}

浙公网安备 33010602011771号