李超线段树

简介

李超线段树通常维护两个操作:

  • 插入一个一次函数

  • 查询直线 x=k 处的先前插入的函数最值

流程

插入

考虑因为插入的都是直线,所以函数在区间 [L,R] 具有单调性。

李超线段树维护的叫做最优势线段,也就是线段树上区间 [L,R],维护的是取 mid=L+R2,所有插入到这一个节点的线段中,k·mid+b 取到最值的线段。

然后在此过程后,一定会有一条直线会被刷掉。由于先前所述的函数单调性,我们只需要比较剩下的直线的端点处的函数值是否比当前线段树上区间最优势线段在端点处的函数值便可以继续递归。

值得一提的是,如果插入的是线段,则可以按普通线段树区间修改一样划分成若干个线段树上的区间去插入。

查询

跟普通线段树的大差不差,具体的见代码。

例题

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

要求插入线段和查询最值。

插入代码:

inline void insert(int &p, int L, int R, int x) {
	if(! x) return ;
	if(! p) p = ++ tot;

	int y = id[p];

	if(x > y) swap(x, y);
	if(F(a[y], mid) > F(a[x], mid)) swap(x, y);

	id[p] = x;

	if(F(a[y], L) > F(a[x], L) || fabs(F(a[y], L) - F(a[x], L)) < eps) insert(lson, y);
	if(F(a[y], R) > F(a[x], R) || fabs(F(a[y], R) - F(a[x], R)) < eps) insert(rson, y);

	return ;
}

inline void add(int l, int r, int x, int &p, int L = 1, int R = INF) {
	if(! p) p = ++ tot;

	if(l <= L && R <= r) {
		insert(p, L, R, x);

		return ;
	}

	if(l <= mid) add(l, r, x, lson);
	if(r > mid) add(l, r, x, rson);

	return ;
}

查询代码:

inline int query(int k, int &p, int L = 1, int R = INF) {
	if(L == R) return id[p];

	int x = id[p], res = 0;

	if(k <= mid) res = query(k, lson);
	else res = query(k, rson);

	if(x > res) swap(x, res);
	if(F(a[res], k) > F(a[x], k)) swap(x, res);

	return x;
}

P4655 [CEOI2017] Building Bridges

首先要拆除的柱子一定是连续的,因此我们想到可以用前缀和去维护这一部分。

dpi 为已经考虑到第 i 位,且第 i 个位置要为桥梁柱的最小代价。

答案:dpn

转移:

考虑枚举断点并由断点 dp 值转移过来,则有:

dpi=min{dpj+(hihj)2+prei1prej}

注意第 i,j 号柱子不能被拆除。

这样时间复杂度是 O(n2) 的,考虑优化。

假设当前考虑到的断点 j 已经是最优的,再拆出平方项,式子化为:

dpi=dpj+hi22hi·hj+hj2+prei1prej

将下标为 i 的项提到一边,下标为 j 的项和下标为 i,j 的交叉项提到另一边,就有:

dpihi2prei1=2hi·hj+dpj+hj2prej

不妨将右边的 hi 看作自变量,2hj 看作斜率,dpj+hj2prej 看作截距,那么李超线段树就可以维护其最小值。

又因为左侧的 hi2prei1 一定是定值,所以当右侧式子最小时,dpi 一定最小。

代码:

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e5 + 5;
int n, x, cnt, h[N], f[N], dp[N], pre[N];
struct Line {
	int k, b;
	Line(int kk = 0, int bb = 0) {
		k = kk, b = bb;
	}
} a[N];

inline int F(Line l, int x) {
	return l.k * x + l.b;
}

namespace Segment_Tree {
	#define mid (L + R) >> 1
	#define son p, L, R
	#define lson ls[p], L, (L + R) >> 1
	#define rson rs[p], ((L + R) >> 1) + 1, R

	int tot, root, ls[N << 2], rs[N << 2], id[N << 2];

	inline void insert(int x, int &p, int L = 0, int R = 1e6) {
		if(! p) p = ++ tot;

		int y = id[p];
		
		if(F(a[y], mid) < F(a[x], mid)) swap(x, y);

		id[p] = x;

		if(F(a[y], L) < F(a[x], L)) insert(y, lson);
		if(F(a[y], R) < F(a[x], R)) insert(y, rson);

		return ;
	}

	inline int query(int x, int &p, int L = 0, int R = 1e6) {
		if(L == R) return F(a[id[p]], x);

		int res = F(a[id[p]], x);

		if(x <= mid) res = min(res, query(x, lson));
		else res = min(res, query(x, rson));

		return res;
	}

	#undef mid
	#undef son
	#undef lson
	#undef rson
}

using namespace Segment_Tree;

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);

	cin >> n;
	for(int i = 1 ; i <= n ; ++ i)
		cin >> h[i];
	for(int i = 1 ; i <= n ; ++ i)
		cin >> x, pre[i] = pre[i - 1] + x;

	a[0] = Line(0, 1e18);

	a[++ cnt] = Line(-2 * h[1], h[1] * h[1] - pre[1]);
	insert(cnt, root);

	for(int i = 2 ; i <= n ; ++ i) {
		// for(int j = 1 ; j <= i - 1 ; ++ j)
		// 	f[i] = min(dp[i], f[j] + (h[i] - h[j]) * (h[i] - h[j]) + pre[i - 1] - pre[j]);

		dp[i] = query(h[i], root) + h[i] * h[i] + pre[i - 1];
		a[++ cnt] = Line(-2 * h[i], dp[i] + h[i] * h[i] - pre[i]);
		insert(cnt, root);
	}

	cout << dp[n];

	return 0;
}
  • UB:更新一个 struct 类型的变量时一定要调用构造函数!
posted @   end_switch  阅读(1)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示