李超线段树
前置知识:一次函数,线段树
什么是李超线段树?
李超线段树,可以支持一下两种操作:
- 插入一条线段/直线;
- 查询单点极值。
李超线段树在每一个区间,会维护在区间中点值最大的线段/直线(表达式:\(y = kx + b\))所对应的 \(k\)、\(b\),且李超线段树没有 pushdown 操作,查询时需要一查到底。
具体实现
加线操作
若需要插入一条线段 \(f\),并且区间 \(\left [l , r \right ]\) 被该线段的 \(x\) 范围完全包含,那么有些区间值就会进行更新。
若 \(f\) 在 $\left \lfloor \frac{l+r}{2} \right \rfloor $ 处比区间原最优线段 \(g\) 更优(或原区间无最优线段),那么就交换 \(f\) 与 \(g\),否则直接进行子树处理:
- 若在 \(l\) 处 \(f\) 更优(说明 \(f\) 与 \(g\) 会在左区间相交),或 \(f\) 线段编号更小,则递归左子树;
- 若在 \(r\) 处 \(f\) 更优(说明 \(f\) 与 \(g\) 会在右区间相交),或 \(f\) 线段编号更小,则递归右子树。
容易得出,最多只会下传左右区间中的一个(或不下传)。
查询操作
从根节点向下找到 \(\left [x, x \right ]\) 的区间,在查找的路径上取最大值所对应的编号。
代码
#include <bits/stdc++.h>
#define PDI pair <DB, int>
#define int long long
#define ST string
#define DB double
#define x first
#define y second
#define ls (x << 1)
#define rs ((x << 1) | 1)
#define mid ((t[x].l + t[x].r) >> 1)
#define fr(x, y, z) for(int x = y; x <= z; x ++ )
#define dfr(x, y, z) for(int x = y; x >= z; x -- )
using namespace std;
const int N = 1e5 + 10, P[2] = {1000000000, 39989};
const DB eps = 1e-9;
int n, tot;
// 封装一个一次函数(y = kx + b)
struct edge
{
DB k, b;
// 计算函数在 x 处的 y 值
DB f(DB x) { return b + k * x; }
// 将输入的 x0,y0,x1,y1 转换成一次函数
void init(int c[])
{
// 特判 x0 = x1 的情况
if(c[1] == c[3]) k = 0, b = 1.0 * max(c[2], c[4]);
else
{
k = (DB)(c[4] - c[2]) / (DB)(c[3] - c[1]);
b = (DB)c[2] - k * (DB)c[1];
}
}
} p[N];
// 线段树记录区间 l,r 与中点处极值所对应的线段编号
struct Node
{ int l, r, mx; } t[160010];
// 初始线段树
void init(int x, int l, int r)
{
t[x] = {l, r, 0};
if(l == r) return ;
init(ls, l, mid);
init(rs, mid + 1, r);
}
// 比较 x 与 y 的大小关系,eps 为 double 产生的精度误差
int cmp(DB x, DB y)
{
if(x - y > eps) return 1;
if(y - x > eps) return -1;
return 0;
}
// 插入线段
void upd(int x, int ins)
{
int &mx = t[x].mx;
int cp = cmp(p[ins].f(mid), p[mx].f(mid));
if(cp == 1 || (!cp && ins < mx)) swap(mx, ins);
int cpl = cmp(p[ins].f(t[x].l), p[mx].f((t[x].l)));
int cpr = cmp(p[ins].f(t[x].r), p[mx].f((t[x].r)));
if(cpl == 1 || (!cpl && ins < mx)) upd(ls, ins); // 情况 1
if(cpr == 1 || (!cpr && ins < mx)) upd(rs, ins); // 情况 2
}
// 寻找线段树上可以插入线段的区间
void upd(int x, int l, int r, int ins)
{
if(l <= t[x].l && t[x].r <= r)
{ upd(x, ins); return ; }
if(l <= mid) upd(ls, l, r, ins);
if(mid < r) upd(rs, l, r, ins);
}
// 计算最大值
PDI pdi_max(PDI x, PDI y)
{
int b = cmp(x.x, y.x);
if(b == 1) return x;
if(b == -1) return y;
return x.y < y.y ? x : y;
}
// 查询极值
PDI query(int x, int d)
{
PDI res = {p[t[x].mx].f(d), t[x].mx};
if(t[x].l == t[x].r) return res;
if(d <= mid) res = pdi_max(res, query(ls, d));
if(mid < d) res = pdi_max(res, query(rs, d));
return res;
}
signed main()
{
init(1, 1, P[1]);
scanf("%lld", &n);
int opt, x, c[5], res = 0;
fr(i, 1, n)
{
scanf("%lld", &opt);
if(opt == 0)
{
scanf("%lld", &x);
x = (x + res - 1 + P[1]) % P[1] + 1;
printf("%lld\n", (res = query(1, x).y));
}
else
{
fr(j, 1, 4) scanf("%lld", &c[j]);
fr(j, 1, 4) c[j] = (c[j] + res - 1 + P[j & 1]) % P[j & 1] + 1;
p[ ++ tot].init(c);
upd(1, min(c[1], c[3]), max(c[1], c[3]), tot);
}
}
return 0;
}
衍生
那么,李超线段树可以拓展什么呢?
那就是 DP,原本需要用凸包维护的斜率优化DP可以直接用李超线段树 + DP 做。
- \(O(N ^ 3)\)
令 \(sumt_i = \sum_{j=1}^{i}t_i\),\(sumc_i = \sum_{j=1}^{i}c[i]\),设 \(dp_{i, j}\) 表示将前 \(i\) 个任务分成 \(j\) 组的方案数,显然有:
\(dp_{i, j} = \min dp_{k, j} + (s \times j + sumt_i) \times (sumc_i - sumc_k)\)。
- \(O(N^2)\)
发现时间复杂度的瓶颈在 \(j\) 的枚举上,而且题目的答案与 \(j\) 的范围无关,所以可以将方程改为:
\(dp_{i, j} = \min dp_{k, j} + (s \times j + sumt_i) \times (sumc_i - sumc_k)\)

浙公网安备 33010602011771号