李超线段树
写在前面
一个博主很早之前学的算法,只记录了自己的模板没写总结,导致在一场考试中,遇到原题但只能含泪写暴力,所以先趁热打铁来写一个博客。
简介
李超线段树,可能才是真正的线段树,因为它真的在记录线段。
用途是来解决一些插入直线/线段,支持查询单点极值的问题。
前置知识
线段树(不会就不用学了)和标记永久化(很简单),关于直线的一些计算(小学生都会)
思路
对于每个线段树节点表示的区间是 \([l,r]\) 则线段树上记录的就是区间中点上的极值的线段标号。(你可能觉得重复记录了一些值,但你发现它非常有用)。
对于一个线段的插入,我们发现不能直接插入线段,因为发现可能会更新它不存在的区间。所以我们需要先找出被它完全覆盖的区间(线段树查询一样的做法),然后就可以直接加入线段了。
对于被它完全覆盖的区间,我们要分讨。
- 如果当前节点没有线段,就直接记录下来,然后返回。
- 1不对,就与线段树上的线段比较在中点位置上的值,选择更优的一个留下,另一个进入三。(这样做,你会发现定义不一定正确,最后留在节点上的不一定最优,因为最优的可能每传下去)
- 此时我们保证了中点处线段树上的是极值,所以考虑另一个线段可能造成贡献的区间。如果左端点低,则说明在左区间有交点,向左区间递归。如果右端点低,则说明在右区间有交点,向右区间递归。如果被完全碾压,就退出。
然后我们考虑如何查询,对于每个点我们都遍历到最底层的节点,取过程中遇到的每一个线段在目标位置上的值中的极值。这里我们就能理解插入为什么是对的。
然后时间复杂度,我们发现插入线段的复杂度是 \(O(\log^2 n)\) (如果是直线就只有一个 \(\log\) ),然后询问是 \(O(\log n)\) 的。
代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10,M=39989,inf=1e9+10;
const double eps=1e-9;
int lasans,cnt,n,sh[N];
struct stu
{
double k,b;
}q[N];
struct node
{
double zhi;int id;
};
double calc(int x,int wz){return q[x].b+q[x].k*wz;}
int cmp(double zhix,double zhik)
{
if(zhix-zhik>eps)return 1;
if(zhik-zhix>eps)return 2;//精度
return 0;
}
void upd(int x,int l,int r,int k)
{
int mid=(l+r)>>1;
int fmid=cmp(calc(sh[x],mid),calc(k,mid));
if(fmid==2||(fmid==0&&sh[x]>k))swap(sh[x],k);
int fl=cmp(calc(sh[x],l),calc(k,l)),fr=cmp(calc(sh[x],r),calc(k,r));
if(fl==2||(fl==0&&sh[x]>k))upd(x<<1,l,mid,k);
if(fr==2||(fr==0&&sh[x]>k))upd(x<<1|1,mid+1,r,k);
return ;
}
void update(int x,int l,int r,int lt,int rt,int k)
{
if(lt<=l&&r<=rt)
{
upd(x,l,r,k);//完全覆盖的区间直接加入
return;
}
int mid=(l+r)>>1;
if(lt<=mid)update(x<<1,l,mid,lt,rt,k);
if(rt>mid)update(x<<1|1,mid+1,r,lt,rt,k);
return ;
}
void add(int x0,int y0,int x1,int y1)//处理出线段信息
{
if(x0==x1){q[++cnt].k=0;q[cnt].b=max(y0,y1);}//处理与y轴平行的线段
else{q[++cnt].k=1.0*(y1-y0)/(x1-x0);q[cnt].b=y0-q[cnt].k*x0;}
}
node nmax(node x,node y)
{
int f=cmp(x.zhi,y.zhi);
if(f==1)return x;
if(f==2)return y;
else return x.id<y.id?x:y;
}
node query(int x,int l,int r,int k)
{
if(r<k||l>k)return {0,0};//不在区间内
int mid=(l+r)>>1;
double zhi=calc(sh[x],k);//是k不是mid
if(l==r)return {zhi,sh[x]};
return nmax({zhi,sh[x]},nmax(query(x<<1,l,mid,k),query(x<<1|1,mid+1,r,k)));
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n;
q[0]={0,-inf};//注意需要先初始0
for(int i=1;i<=n;i++)
{
int op;
cin>>op;
if(op==1)
{
int x0,y0,x1,y1;
cin>>x0>>y0>>x1>>y1;
x0=(x0+lasans-1)%39989+1;x1=(x1+lasans-1)%39989+1;
y0=(y0+lasans-1)%1000000000+1;y1=(y1+lasans-1)%1000000000+1;//强制在线
if(x0>x1)swap(x0,x1),swap(y0,y1);//保证左右端点
add(x0,y0,x1,y1);//增加一个线段。
update(1,1,M,x0,x1,cnt);
}
else
{
int x;cin>>x;
x=(x+lasans-1)%39989+1;
cout<<(lasans=query(1,1,M,x).id)<<'\n';
}
}
return 0;
}
易错点
- 要有初始的哨兵(初始化0号的值)
- 有些题要注意x1,x2的大小关系
- 使用可能需要考虑精度问题
例题
3.15考试题
大概就是插入线段,求单点最小值。
与模板题基本一致,改一下极值判断即可。同时因为值域为 \(1e9\) 所以请使用动态开点线段树。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10,inf=1e9;
const double eps=1e-8;
struct node
{
int x1,y1,x2,y2;
double k;
}f[N];
struct stu
{
int db,ls,rs;
}sh[N*31];//开够了吗
int m,cnt,tot,rt;//不要混用
double js(int j,int a)
{
// cout<<j<<" "<<a<<" "<<f[j].y1<<" "<<f[j].k*(a-f[j].x1)<<'\n';
return 1.0*f[j].y1+f[j].k*(a-f[j].x1);
}
double query(int x,int l,int r,int wz)
{
if(x==0)return inf+10;
int mid=(l+r)>>1;double res=inf+10;
//cout<<x<<" "<<l<<" "<<r<<'\n';
if(sh[x].db)
{
// cout<<x<<" "<<l<<" "<<r<<" "<<sh[x].db<<'\n';
res=js(sh[x].db,wz);//想清楚求得是什么
}
if(l==r)return res;
if(wz<=mid)return min(res,query(sh[x].ls,l,mid,wz));
else return min(res,query(sh[x].rs,mid+1,r,wz));
}
int check(int wz,int u,int v)
{
double zu=js(u,wz),zv=js(v,wz);
if(zv-zu>eps)return 2;//注意精度问题
if(zu-zv>eps)return 1;
return 0;
}
void insert(int& x,int l,int r,int z)
{
//cout<<l<<' '<<r<<'\n';
if(!x)x=++tot;
if(!sh[x].db)
{
//cout<<x<<" "<<l<<" "<<r<<'\n';
sh[x].db=z;return;
}//不存在结束
int mid=(l+r)>>1;
int zj=check(mid,sh[x].db,z);
if(zj==1)swap(sh[x].db,z);//如果当前节点在中间比上一个数低
int zb=check(l,sh[x].db,z),yb=check(r,sh[x].db,z);
if(zb==1)insert(sh[x].ls,l,mid,z);//左边比代表低
if(yb==1)insert(sh[x].rs,mid+1,r,z);//右边比代表低
return;
}
void modify(int& x,int l,int r,int lt,int rt,int z)
{
if(!x) x=++tot;
if(lt<=l&&rt>=r)
{
insert(x,l,r,z);//抓出区间
return ;
}
int mid=(l+r)>>1;
if(lt<=mid)modify(sh[x].ls,l,mid,lt,rt,z);
if(rt>mid)modify(sh[x].rs,mid+1,r,lt,rt,z);
}
int main()
{
freopen("defense.in","r",stdin);
freopen("defense.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>m;
for(int i=1;i<=m;i++)
{
int opt;cin>>opt;
if(opt==1)
{
cnt++;
cin>>f[cnt].x1>>f[cnt].y1>>f[cnt].x2>>f[cnt].y2;
f[cnt].k=(f[cnt].y2-f[cnt].y1)*1.0/(f[cnt].x2-f[cnt].x1)*1.0;
//cout<<f[i]
modify(rt,1,inf,f[cnt].x1,f[cnt].x2,cnt);
}
else
{
int a;cin>>a;
cout<<query(rt,1,inf,a)<<'\n';
}
}
return 0;
}
模板
如果你非常擅长 \(dp\) 的话,你一定做过斜率优化的题。
斜率优化是形如这样的\(dp[i]=Min/Max(a[i]∗b[j]+c[j])+d[i]\)的dp转移式子。
在除了主流的维护凸包的做法,我们换一种理解方式。如果上面那个式子理解为一个函数,显然是一个一次函数\(y=kx+b\)的式子,对于每个 \(j\),\(b[j]\)就是 \(k\),\(c[j]\)就是 \(b\),\(a[i]\) 就是 \(x\)。然后求极值,不就是一堆直线(函数但函数图像就是直线)求单点最值,无脑上李超线段树即可。复杂度为 \(O(n\log n)\)。
复杂度不一定是最优的,但是非常无脑只用推出式子,不用任何性质,还非常好写。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+10,inf=1e18+10;
int n,sum[N],h[N],dp[N],sh[N<<2],mx;
struct node
{
int k,b;
}bi[N];
int cmp(int x,int y){return x>y;}
int calc(int k,int wz){return bi[k].k*wz+bi[k].b;}
void modify(int x,int l,int r,int k)
{
int mid=(l+r)>>1;
int fmid=cmp(calc(sh[x],mid),calc(k,mid));//比较sh[x]代表的线段与k代表的线段在mid处的大小关系
if(fmid==1)swap(sh[x],k);//保证中点的地方sh[x]比k低
int fl=cmp(calc(sh[x],l),calc(k,l)),fr=cmp(calc(sh[x],r),calc(k,r));
if(fl==1)modify(x<<1,l,mid,k);//如果与左区间有交点
if(fr==1)modify(x<<1|1,mid+1,r,k);//如果与右区间有交点
return ;
}
node nmix(node x,node y)
{
int f=cmp(x.k,y.k);
if(f)return y;
return x;
}
node query(int x,int l,int r,int k)
{
if(r<k||l>k)return {inf,inf}; //不在范围
int mid=(l+r)>>1;
int zhi=calc(sh[x],k);//计算在x的位置
if(l==r){return {zhi,sh[x]};}//在子节点
return nmix({zhi,sh[x]},nmix(query(x<<1,l,mid,k),query(x<<1|1,mid+1,r,k)));
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)cin>>h[i],mx=max(h[i],mx);
for(int i=1;i<=n;i++)
{
int x;cin>>x;
sum[i]=sum[i-1]+x;
}
bi[0]={0,inf};
modify(1,0,mx,0);
bi[1].k=-2*h[1],bi[1].b=dp[1]+h[1]*h[1]-sum[1];//第一根柱子的情况
modify(1,0,mx,1);
for(int i=2;i<=n;i++)
{
int k=query(1,0,mx,h[i]).k;
dp[i]=k+sum[i-1]+h[i]*h[i];
bi[i].k=-2*h[i],bi[i].b=dp[i]+h[i]*h[i]-sum[i];
modify(1,0,mx,i);
}
cout<<dp[n]<<'\n';
return 0;
}
拓展
其实还有一个广义李超线段树。
我们发现其实李超线段树并不依赖直线,只需要满足两个性质:
- 任意两个函数只有一个交点
- 可以快速求出函数某个位置的值
所以在某些题中,如果满足这些性质也可以仿照李超线段树的写法来做。
详见这里

浙公网安备 33010602011771号