李超线段树
基本概念
李超线段树是由学军中学队爷李超提出的一种数据结构,是线段树的一个变种。李超线段树可以维护函数定点最值,它通常用来处理这种类型的问题:每次可以在平面上加入一条线段,试求与直线 \(x = k\) 相交的的线段中,交点纵坐标最大(小)的线段编号。
李超线段树不需要更新结点信息和下传标记,它的单次修改时间复杂度为 \(O(log^2n)\),单次查询时间复杂度为 \(O(logn)\) 。由于笔者不擅长数学,因此本文默认读者已经掌握了基本的几何只是。
算法思想
李超线段树维护 \(x\) 轴区间所对应的 最优势线段。最优势线段的定义为在区间中点处的最高线段,或者是暴露在区间最高折线中且横坐标跨度最大的线段。我们维护区间的最优势线段,每次查询位置 \((x, +\infty)\) 时直接取所有包含该位置的区间最优势线段的最大值即可。
如上图,区间 \([L, R]\) 的最优势线段为红色线段。
假设某区间 \([L, R]\) 原本的最优势线段为 \(l_1\),现在要加入的线段为 \(l_2\)。考虑维护该区间新的最优势线段,分类讨论:
- \(l_1\) 的两端点均比 \(l_2\) 对应的端点高,直接取 \(l_1\) 作为区间最优势线段。
- \(l_1\) 的两端点均比 \(l_2\) 对应的端点低,更新区间 \([L, R]\) 的最优势线段为 \(l_2\)。
-
\(l_1\) 与 \(l_2\) 有交点,分类讨论:
- 如果 \(l_2\) 与区间左端点的交点高于 \(l_1\) 与区间左端点的交点,此时若 \(l_1\) 与 \(l_2\) 的交点在区间左半侧,说明线段 \(l_2\) 可能无法成为区间右半侧的最优势线段。因此不更新区间 \([L, R]\) 的最优势线段,用线段 \(l_2\) 递归维护左半区间的最优势线段。
- 反之,说明 \(l_2\) 作为区间最优势线段比 \(l_1\) 更优,此时区间左半侧的最优势线段为 \(l_2\),用 \(l_1\) 递归维护右半区间的最优势线段。
- 如果 \(l_2\) 与区间右端点的交点高于 \(l_1\) 与区间右端点的交点,此时若 \(l_1\) 与 \(l_2\) 的交点在区间右半侧,说明线段 \(l_2\) 可能无法成为区间左半侧的最优势线段。因此不更新区间 \([L, R]\) 的最优势线段,用线段 \(l_2\) 递归更新右半区间的最优势线段。
- 反之,说明 \(l_2\) 作为区间最优势线段比 \(l_1\) 更优,此时区间右半侧的最优势线段为 \(l_2\),用 \(l_1\) 递归维护左半区间的最优势线段。
接着我们考虑一些具体的代码细节。首先,我们需要利用两点式和斜截式,存储输入直线的斜率。每次更新时利用斜率来求出两线段之间的交点以及线段在 \(x\) 处的 \(y\) 坐标。因为每次更新我们都会将区间折半维护,因此如果遇到了叶子结点,我们需要特判递归边界。因为每次加入线段最多会有 \(logn\) 个区间的贡献变化,而每次贡献变化修改的区间不超过 \(logn\) 个,所以单词修改时间复杂度为 \(O(log^2n)\),我们要相信李超线段树的常数真的很小,因为它还能套树链剖分过掉 \(1\) 秒 \(10\) 万……详见代码。
参考代码
#include <cstdio>
#include <utility>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 5;
const int modx = 39989;
const int mody = 1e9;
struct node
{
int l, r, cur;
bool flag;
} tree[maxn << 2];
int n, lcnt;
pair<double, double> line[maxn];
double f(pair<double, double> l, int x)
{
return l.first * x + l.second;
}
double inter(pair<double, double> x, pair<double, double> y)
{
return (y.second - x.second) / (x.first - y.first);
}
int get_max(int x, int a, int b)
{
if (f(line[a], x) > f(line[b], x))
return a;
return b;
}
void build(int k, int l, int r)
{
tree[k].l = l;
tree[k].r = r;
if (l == r)
return;
int mid = (l + r) / 2;
build(2 * k, l, mid);
build(2 * k + 1, mid + 1, r);
}
void update(int k, int x, int l, int r)
{
if (tree[k].r < l || tree[k].l > r)
return;
int mid = (tree[k].l + tree[k].r) / 2;
if (tree[k].l >= l && tree[k].r <= r)
{
if (!tree[k].flag)
{
tree[k].cur = x;
tree[k].flag = true;
return;
}
double ly = f(line[tree[k].cur], l), ry = f(line[tree[k].cur], r);
double ly1 = f(line[x], l), ry1 = f(line[x], r);
if (ly1 <= ly && ry1 <= ry)
return;
if (ly1 >= ly && ry1 >= ry)
{
tree[k].cur = x;
return;
}
double point = inter(line[tree[k].cur], line[x]);
if (ly1 >= ly)
{
if (point <= mid)
update(2 * k, x, l, r);
else
{
update(2 * k + 1, tree[k].cur, l, r);
tree[k].cur = x;
}
}
else
{
if (point > mid)
update(2 * k + 1, x, l, r);
else
{
update(2 * k, tree[k].cur, l, r);
tree[k].cur = x;
}
}
return;
}
if (l <= mid)
update(2 * k, x, l, r);
if (r > mid)
update(2 * k + 1, x, l, r);
}
int query(int k, int x)
{
int res = 0;
if (tree[k].flag)
res = get_max(x, res, tree[k].cur);
if (tree[k].l == tree[k].r)
return res;
int mid = (tree[k].l + tree[k].r) / 2;
if (x <= mid)
res = get_max(x, res, query(2 * k, x));
else
res = get_max(x, res, query(2 * k + 1, x));
return res;
}
int main()
{
int opt, ans = 0;
int xa, ya, xb, yb, k;
double val;
scanf("%d", &n);
build(1, 1, 40000);
for (int i = 1; i <= n; i++)
{
scanf("%d", &opt);
if (opt)
{
scanf("%d%d%d%d", &xa, &ya, &xb, &yb);
xa = (xa + ans - 1) % modx + 1;
ya = (ya + ans - 1) % mody + 1;
xb = (xb + ans - 1) % modx + 1;
yb = (yb + ans - 1) % mody + 1;
if (xa ^ xb)
{
val = double(yb - ya) / (xb - xa);
line[++lcnt] = make_pair(val, -val * xa + ya);
}
else
line[++lcnt] = make_pair(.0, (double)max(ya, yb));
update(1, lcnt, min(xa, xb), max(xa, xb));
}
else
{
scanf("%d", &k);
k = (k + ans - 1) % modx + 1;
ans = query(1, k);
printf("%d\n", ans);
}
}
return 0;
}