Loading

李超线段树

前置知识:一次函数,线段树

什么是李超线段树?

李超线段树,可以支持一下两种操作:

  1. 插入一条线段/直线;
  2. 查询单点极值。

李超线段树在每一个区间,会维护在区间中点值最大的线段/直线(表达式:\(y = kx + b\))所对应的 \(k\)\(b\),且李超线段树没有 pushdown 操作,查询时需要一查到底。

具体实现

加线操作

若需要插入一条线段 \(f\),并且区间 \(\left [l , r \right ]\) 被该线段的 \(x\) 范围完全包含,那么有些区间值就会进行更新。

\(f\) 在 $\left \lfloor \frac{l+r}{2} \right \rfloor $ 处比区间原最优线段 \(g\) 更优(或原区间无最优线段),那么就交换 \(f\)\(g\),否则直接进行子树处理:

  1. 若在 \(l\)\(f\) 更优(说明 \(f\)\(g\) 会在左区间相交),或 \(f\) 线段编号更小,则递归左子树;
  2. 若在 \(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 做。

例题:P5785 [SDOI2012] 任务安排

  1. \(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)\)

  1. \(O(N^2)\)

发现时间复杂度的瓶颈在 \(j\) 的枚举上,而且题目的答案与 \(j\) 的范围无关,所以可以将方程改为:

\(dp_{i, j} = \min dp_{k, j} + (s \times j + sumt_i) \times (sumc_i - sumc_k)\)

posted @ 2025-07-17 20:25  Hyvial  阅读(47)  评论(0)    收藏  举报