李超线段树 学习笔记
李超线段树 学习笔记
引入
最近一直在做斜率优化的题,然而只会傻傻维护凸包,一到横坐标不单调,就涉及到手打平衡树,但是我实在不想学平衡树了,所以就准备掏出解决处理直线的大宝贝——李超线段树
功能
有两种操作:
插入一条表达式为 $ L : y = k\times x+b$ 的直线,给出 \(k,b\)。
给出 \(t\),求当前所有直线中与直线 \(x = t\) 交点的纵坐标最大是多少
实现
这里先从“斜率优化”问题中的 “插入的为直线而不是线段” 的简单情况说起。
原理
维护区间的中点对应的最大值的直线,用到了标记永久化的思想。
upd2025.5.7:
所谓标记永久化,就是没有下放操作的线段树,查询的时候访问所有可能对答案产生贡献的节点,也就是单点查询的时候一条路 \(O(\log n)\) 走到底。
维护方法
修改
(偷了两张CSDN博主的图)

如当前情况,蓝色的直线是我们加入的直线,如果它比这个区间所有的都大(完全覆盖),那么就把原有线段换成这条直线;反之则直接舍弃。
下一种情况就是不完全覆盖(有交点)

-
首先对于当前这个大区间,现在应该取 \(f(mid )\) 更大的直线作为当前区间的标记,那么对于整个区间都是如此吗?
-
No,可以看到两条直线在 \([l,mid]\) 之间是存在交点的, 那么在左区间的某个位置,\(f(mid')\) 的大小关系就可能发生变化,所以我们需要递归进入有交点的区间来进一步修改
查询
和标记永久化的线段树查询一样,我们查询一个位于 \(x=t\) 的点对应的最大函数值,就要一直从 \([1,n]\) 的直线,一直查询到 \([t,t]\) 的直线,最后取所有可能的最大值,如下图:

我们要把橙色、绿色、蓝色、黄色区间的直线之间所有的最大值都取到。
例题
分析
例题用Build Bridges作为板子,本题写出来的状态转移方程是:
按照正常斜率优化的套路写出来的话,原式就变成了:
这时候你发现我们要作为斜率的\((-2h_i)\)并不单调了,而且插入的点 \((h_i,f_i+h_i^2-sumw_i)\)横坐标也不是单调的,所以这个时候就要把方程换一种写法然后使用李超线段树了:
可以发现前面是常数项,后面很像一个直线的形式了,我们可以看成给出一个横坐标,求 \(y=(-2h_j)x+(f_j+h_j^2-sumw_j)\)的最小值。
其中我们每给出一条直线,就看成插入 \(k_i=-2h_i,b_i=f_i+h_i^2-sumw_i\)的一条\(y=k_ix+b_i\)的直线,并且在插入之前查询之前存在的所有直线,使得当前的横坐标\(h_i\)对应能取得最小值对应的函数值,然后再插入这条直线。
注意一开始要让\(k[0]=b[0]=INF\),不然第一条直线甚至都无法插入
Code
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define int long long
using namespace std;
const int maxn=1e5+100;
const int maxm=1e6+10;
int n,h[maxn],w[maxn],sumw[maxn],f[maxn];
int k[maxn],b[maxn];
int rt[maxm<<2];
inline int calc(int x/*横坐标*/,int idx/*编号*/){return k[idx]*x+b[idx];}
inline int K(int j){return -2*h[j];}
inline int B(int j){return f[j]+h[j]*h[j]-sumw[j];}
inline int X(int i){return h[i];}
inline void insert(int x,int l,int r,int idx)
{
if(l==r){if(calc(l,idx)<calc(l,rt[x]))rt[x]=idx;return ;}
int mid=l+r>>1;
if(calc(mid,idx)<calc(mid,rt[x]))swap(idx,rt[x]);//完全覆盖
if(calc(l,idx)<calc(l,rt[x]))insert(x<<1,l,mid,idx);//左区间可能更新
if(calc(r,idx)<calc(r,rt[x]))insert(x<<1|1,mid+1,r,idx);//右区间可能更新
}
inline int query(int x,int l,int r,int x_given)
{
if(l==r)return calc(x_given,rt[x]);
int mid=l+r>>1;
int nex=x_given<=mid?query(x<<1,l,mid,x_given):query(x<<1|1,mid+1,r,x_given),now=calc(x_given,rt[x]);
return min(now,nex);//所有包含x_given 的最大值并
}
void input()
{
scanf("%lld",&n),b[0]=1e18;
for(register int i=1;i<=n;++i)scanf("%lld",&h[i]);
for(register int i=1;i<=n;++i)scanf("%lld",&w[i]),sumw[i]=sumw[i-1]+w[i];
k[1]=K(1),b[1]=B(1),insert(1,0,maxm,1);
}
signed main()
{
input();
for(register int i=2;i<=n;++i)
{
f[i]=h[i]*h[i]+sumw[i-1]+query(1,0,maxm,X(i));
k[i]=K(i),b[i]=B(i),insert(1,0,maxm,i);
}
printf("%lld",f[n]);
return 0;
}
注意
写斜率优化的时候完全不用考虑直线的左右端点,直接插入和查询就是了,因为你插入的这个直线一定是无限长的,所以可以把所有的横坐标都囊括,因为我们用当前线段去更新这个区间的前提,就是这个区间被完全包括在这个线段之内。
但是如果插入直线的时候并不是无限长的,我们就要先按照定义找到被完全包含于这条直线的线段区间,然后再对它以及它的子线段递归进行更新。
普通李超线段树的插入写出来就是这样的:
(upd2025.5.7:以下代码是中学时期随便口胡的不保证正确性)
struct SegmentTree{int idx,l,r;}rt[x];
int k[maxn],b[maxn];
int calc(int x,int idx){return 1ll*k[idx]+b[idx];}
inline void modify(int x,int l,int r,int idx)
{
int mid=rt[x].l+rt[x].r>>1;
if(l<=rt[x].l&&rt[x].r<=r)
{
if(calc(mid,rt[x].idx)>calc(mid,idx))rt[x].idx=idx;
modify(x<<1,l,r,idx),modify(x<<1|1,l,r,idx);
}
if(rt[x].l==rt[x].r)
{
if(calc(mid,rt[x].idx)>calc(mid,idx))rt[x].idx=idx;
return ;
}
if(l<=mid)modify(x<<1,l,r,idx);
if(r>mid)modify(x<<1|1,l,r,idx);
}
以下是大学之后写了一下一般李超线段树之后造出来的代码,这时比插入无限长直线多了一个 \(O(\log n)\) 找区间的过程,所以修改是 \(O(\log{n^2})\) 的复杂度。
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
const int INF=1e18;
const double eps=1e-9;
#define double long double
double k[N],b[N];
int cnt;
inline void new_seg(int x,int y,int xx,int yy)
{
++cnt;
if(x==xx)k[cnt]=0,b[cnt]=1.0*max(y,yy);
else k[cnt]=1.0*(1.0*yy-1.0*y)/(1.0*xx-1.0*x),b[cnt]=1.0*y-k[cnt]*1.0*x;
// cout<<"NEW:"<<cnt<<' '<<k[cnt]<<' '<<b[cnt]<<'\n';
}
inline double calc(double x,int id){return 1.0*x*k[id]+b[id];}
int tr[N];
inline int cmp(double a,double b)
{
if(a-b>eps)return 1;
if(b-a>eps)return -1;
return 0;
}
inline void modify(int x,int l,int r,int id)
{
int mid=l+r>>1;
int ck=cmp(calc(mid,id),calc(mid,tr[x]));
if(ck==1||(ck==0&&id<tr[x]))swap(tr[x],id);//
ck=cmp(calc(l,id),calc(l,tr[x]));
if(ck==1||(ck==0&&id<tr[x]))modify(x<<1,l,mid,id);//
ck=cmp(calc(r,id),calc(r,tr[x]));
if(ck==1||(ck==0&&id<tr[x]))modify(x<<1|1,mid+1,r,id);//
}
//相等时,但是序号相对来说更小 也需要更新,特别注意浮点数特殊的大小比较
inline void position(int x,int l,int r,int modl,int modr,int id)
{
if(modl<=l&&r<=modr)return modify(x,l,r,id),void();
int mid=l+r>>1;
if(modl<=mid)position(x<<1,l,mid,modl,modr,id);
if(modr>mid)position(x<<1|1,mid+1,r,modl,modr,id);
}
inline pair<int,double>max_pair(pair<int,double> a,pair<int,double> b)
{
int ck=cmp(a.second,b.second);
if(ck==1)return a;
else if(ck==-1)return b;
else return a.first<b.first?a:b;
}
inline pair<int,double> query(int x,int l,int r,int pos)
{
if(l==pos&&r==pos)return make_pair(tr[x],calc(pos,tr[x]));
int mid=l+r>>1;
pair<int,double> ans=make_pair(tr[x],calc(pos,tr[x]));
if(pos<=mid)ans=max_pair(ans,query(x<<1,l,mid,pos));
else ans=max_pair(ans,query(x<<1|1,mid+1,r,pos));
return ans;
}
const int MOD1=39989,MOD2=1e9;
int main()
{
b[0]=-INF;
ios::sync_with_stdio(0);
cin.tie(0);
int q,opt,pos;
cin>>q;
int lastans=0;
while(q--)
{
cin>>opt;
if(opt==0)
{
cin>>pos;
pos=(pos+lastans-1)%MOD1+1;
lastans=query(1,1,40000,pos).first;
cout<<lastans<<'\n';
}
else
{
int x,xx,y,yy;
cin>>x>>y>>xx>>yy;
x=(x+lastans-1)%MOD1+1;
xx=(xx+lastans-1)%MOD1+1;
y=(y+lastans-1)%MOD2+1;
yy=(yy+lastans-1)%MOD2+1;
new_seg(x,y,xx,yy);
if(x>xx)swap(x,xx);
position(1,1,40000,x,xx,cnt);
}
}
return 0;
}
复杂度
综上,斜率优化写的李超树只是李超线段树的一种特殊情况(每条直线的端点就都是整个区间的左右端点)。
我们在写普通李超线段树的插入操作的时候,把区间分割成一个个能被完全包含的小区间的复杂度是 \(O(\log n)\) 的,然后对于每一个这样的区间,我们还要分别对其进行一次 \(O(\log n)\) 的更新,所以总的复杂度是 \(O(\log^2n)\) 的,而询问的就是对于所在的所有区间的最大值/最小值取并集,所以就是 \(O(\log n)\) 的。
插入:\(O(\log^2n)\)
查询:\(O(\log n)\)
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/16829796.html

浙公网安备 33010602011771号