李超线段树

李超线段树

线段树的扩展版本

用来动态维护平面上直线

支持插入直线/线段,查询横坐标某处覆盖的线中纵坐标最大/最小值

可以用于斜率优化 DP

插入直线

这里直线是线段树上维护的信息,表示当前区间中点处最优的直线

同时相当于区间修改,直线也是标记,这个区间内都可能有这条直线

但是标记难以合并,必须递归下传

有时两条直线在区间的中间产生交点,无法确定子节点需要哪个

因此利用标记永久化的思想,只有在当前区间中点不优且可能成为子节点区间中点更优的直线才下传,每个节点上存的直线不一定是当前在它中点处最优的,但是如果有更优的,一定会在根节点到它路径上某处被记录

假设在 [l,r] 中点 mid 处原有直线 x 更优,新增直线为 y,如果相反则交换 x,y

  • 如果 l,rx 都比 y 优,y 在整个区间内不可能比 x 优,直接返回
  • 如果 ly 更优,则递归至 [l,mid],如果 ry 更优,则递归至 [mid+1,r]

因为插入的是直线,第二种情况中只会有一个成立,最多递归一边,复杂度是 O(logn)

注意:动态开点李超树的空间是 O( 插入直线次数 ) 的,因为每次插入一条直线时,只会更新一条链,最多使最下面新增 O(1) 个节点

插入线段

由于线段只在某一区间内生效,因此需分成 O(logn) 个区间,每个区间都递归下传,复杂度为 O(log2n)

查询

比较简单,直接找到那个点,注意要一路比较取最优的

P4097 【模板】李超线段树 / [HEOI2013] Segment

struct seg
{
	double k, b;	int id; 
}lin[M];
double calc(int x, int p)	{return lin[p].k * x + lin[p].b;}
int chkmax(int x, int p, int q) // 点 x 处 p,q 哪条直线取值更优
{
	if(!p || !q)	return p + q; 
	double val1 = calc(x, p), val2 = calc(x, q);
	return sgn(val1 - val2) > 0 ? p : (sgn(val1 - val2) < 0 ? q : (lin[p].id > lin[q].id ? q : p)); 
}
struct lctree
{
	int num[N << 2];
	int lson(int x)	{return x << 1;}
	int rson(int x)	{return x << 1 | 1;}
	void upd(int l, int r, int p, int x) // 递归下传
	{
		if(l == r)	{num[p] = chkmax(l, num[p], x);	return;}
		int mid = (l + r) >> 1;
		if(chkmax(mid, num[p], x) == x)	swap(num[p], x);
		if(chkmax(l, num[p], x) == x)	upd(l, mid, lson(p), x);
		if(chkmax(r, num[p], x) == x)	upd(mid + 1, r, rson(p), x);
	}
	void update(int l, int r, int nl, int nr, int p, int x) // 拆分线段
	{
		if(l <= nl && nr <= r)	return upd(nl, nr, p, x);
		int mid = (nl + nr) >> 1;
		if(mid >= l)	update(l, r, nl, mid, lson(p), x);
		if(mid < r)	update(l, r, mid + 1, nr, rson(p), x);
	}
	int query(int id, int l, int r, int p)
	{
		if(l == r)	return num[p];
		int mid = (l + r) >> 1, res = num[p];
		if(mid >= id)	return chkmax(id, res, query(id, l, mid, lson(p)));
		return chkmax(id, res, query(id, mid + 1, r, rson(p)));
	}
}tree;

线段树合并

李超树也可以合并!

按正常线段树合并的流程,合并叶子就直接取最优的,注意合并后要把另一棵线段树上对应节点的直线当作标记下传更新

复杂度有大佬说时 O(nlogn) 的,用了势能证明,我不会

应用

斜率优化

CF932F Escape Through Leaf

DP 方程不难列出:

fi=minjsubtreei{fj+ai×bj}

i,j 的乘积项,看着很斜率优化,如果把 fj 当作 bbj 当作 kai 当作 xfi 当作 y

则式子是 y=kx+b 的形式,用李超线段树插入直线,维护最小值

但是 j 必须在 i 子树内,可以用李超线段树合并,复杂度为 O(nlogn),还可以用 DSU on tree,复杂度为 O(nlog2n)

ll calc(ll id, ll x)	{return id ? (x - D) * lin[id].k + lin[id].b : inf;} 
struct lctree
{
	ll ls[N * 20], rs[N * 20], num[N * 20], idx; // 动态开点
	ll update(ll l, ll r, ll p, ll id)
	{
		if(!p)	p = ++idx;
		if(!num[p])	{num[p] = id;	return p;}
		ll mid = (l + r) >> 1;
		if(calc(id, mid) < calc(num[p], mid))	swap(num[p], id);
		if(calc(num[p], l) > calc(id, l))	ls[p] = update(l, mid, ls[p], id);
		if(calc(num[p], r) > calc(id, r))	rs[p] = update(mid + 1, r, rs[p], id);
		return p;
	}
	ll merge(ll x, ll y, ll l, ll r)
	{
		if(!x || !y)	return x + y;
		if(l == r)	return calc(num[x], l) > calc(num[y], l) ? y : x; 
		ll mid = (l + r) >> 1;
		ls[x] = merge(ls[x], ls[y], l, mid);
		rs[x] = merge(rs[x], rs[y], mid + 1, r);	
		return update(l, r, x, num[y]); // 注意下传!
	}
	ll query(ll x, ll l, ll r, ll p)
	{
		if(!p)	return inf;
		ll res = calc(num[p], x);
		if(l == r)	return res;
		ll mid = (l + r) >> 1;
		if(mid >= x)	return min(res, query(x, l, mid, ls[p]));
		return min(res, query(x, mid + 1, r, rs[p]));
	}
}tree;
void dfs(ll x, ll fa)
{
	for(ll y : edge[x])
		if(y != fa)	dfs(y, x), root[x] = tree.merge(root[x], root[y], 0, V);
	if(root[x])	f[x] = tree.query(a[x] + D, 0, V, root[x]);
	lin[++cnt] = (line){b[x], f[x]};
	root[x] = tree.update(0, V, root[x], cnt);
}
int main()
{
	read(n);
	for(ll i = 1; i <= n; ++i)	read(a[i]);
	for(ll i = 1; i <= n; ++i)	read(b[i]);
	for(ll i = 1; i < n; ++i)	read(u), read(v), edge[u].pb(v), edge[v].pb(u);
	dfs(1, 0);
	for(ll i = 1; i <= n; ++i)	print(f[i]), putchar(' ');
	return 0;
}
posted @   KellyWLJ  阅读(101)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示