学习NOTE——10 李超线段树
李超真是个____。这都想得到。
引入(?)
我们要在平面直角(笛卡尔)坐标系中进行一些操作:
- 动态插入一条线段(或直线);
- 单点查询:给定一个横坐标\(x\),查询所有已插入线段在该点处的纵坐标最大值,并输
出对应线段编号。
但是朴素的暴力在每次查询时都是\(O(n)\)的,肥肠的不优。
但是,李超线段树可以在\(O(\log{C})\)的时间里做到!(\(C\)为坐标范围大小)
前置芝士:线段树,一次函数
是什么
李超线段树(Li Chao Tree)是一种可以求函数定点最值的线段树,代码 一点都不 简短,思想 一点都不 简明,用途 是真的 广泛。
既然叫“线段树”,那么写法是差不多的,只是加了一些数学小知识而已(吗)。
怎么实现
到重点了!!
存线段
- 首先,要存每条线的信息吧。
两种办法:
- 存起点与终点的坐标 \((x1,y1),(x2,y2)\)
struct Line{int x0,y0,x1,y1;}l[N]; - 存斜率(比例系数)与截距(一般用于 dp 题)
struct Line{ int k,b; Line(){k=0,b=0;}//赋初值 //主要目的就是建一个哨兵节点,防止访问到空的 }l[N];
- 既然是线段树,那肯定要存线段树的节点啊。
struct Tree{int l,r,bst;}tr[N<<2]; //这里的bst是指当前节点代表区间的最优线段编号
对于最优线段的解释

这里对于区间 \([0,8]\) 的最优线段是绿色的,为什么是绿色的呢?因为李超线段树维护的是在当前区间中点处最大/小的。
为什么是中点
选择中点是为了保证树的平衡性和操作的正确性。通过比较中点处的函数值,
可以决定新插入的线段是否应该“覆盖”当前节点存储的线段,或者递归到子区间。
插入
大概的流程:对于每条线段,我们都不断二分(像线段树一样),直到它完全覆盖了一些区间(且没有别的空白),再在这些区间内更新最优线段,最后查看被淘汰下来的线段在哪里更优,就递归哪一棵子树。
模拟一下:
考虑在一个\([1,20]\)的区间内进行操作,加入一条蓝线。

不断二分:

接着判断最优,既然没有别的线段,那就直接加。(剩下的因为我太懒就不画了)

加入一条绿线:

依旧二分。
如果绿线完全覆盖了每个区间,但蓝线没有的(比如\([1,5]\)),直接更新为绿色的。但如果两个都覆盖了的(比如\([6,10]\)),那我们就要去比较一下,别忘了是在中点处比较。
最后看淘汰掉的线有没有在左/右子树中更优(这可不能忘了)。(这里蓝色的是蓝线,绿色的是绿线,黄色的是被修改过的)(依旧不画完)

总结一下:
- 若当前区间还没记录最优线段,那么直接记录并返回。
- 若当前区间的最优线段新插入的线段完全覆盖,则把最优线段修改为新插入线段并返回。
- 若当前区间的最优线段把新插入线段完全覆盖,则直接返回。
- 若当前区间最优势线段与新插入线段有交,则先判断哪条线段在当前区间更优,并把不优的线段下
传到交点所在子区间。(交点两边的部分分别被这两条线控制,既然我们已经让中点处更优的那条
线段作为最优线段,因此不优的那条只可能在交点所在子区间超过最优
线段)
查询
查询就很简单啦,就像普通线段树一样,走到底,然后找最优线段。因为李超线段树采用了标记永久化,每个节点存储的线段并不一定是全局在该点处最优的,则需要从根节点到叶子节点路径上所有节点的优势线段中取最优。
就像这样:比如说要查 \(x=3\) 的最优线段,直接走到底:

代码
首先是关于线段的信息与计较,存的话可以存\((x_0,y_0),(x_1,y_1)\),也可以存\(k,b\)(上面的存什么板块也讲了)。
既然我们知道了线段的信息,那要求一个在线段上的\(y\)该怎么求呢?有个公式 (但我不会推QAQ):\(y=\frac{(x-x_0)(y_0-y_1)}{x_0-x_1}\),注意精度问题。
然后是比较,我们可以直接比在当前这个\(x\),谁的\(y\)更优。
struct Line{int x0,y0,x1,y1;}l[N];
struct Tree{int l,r,bst;}tr[N<<2];
double _y(int p,int x){//计算x在线段p上的纵坐标
if(l[p].x0==l[p].x1) return max(l[p].y0,l[p].y1);//x0==x1
return 1.0*(x-l[p].x0)*(l[p].y0-l[p].y1)/(l[p].x0-l[p].x1)+l[p].y0;
}
int cmp(double a,double b){//比较哪y个更大,相同则序号小的优先
double ep=1e-9;
if(a-b>ep) return 1;
if(b-a>ep) return -1;
return 0;
}
int pmx(int a,int b,int x){
int c=cmp(_y(a,x),_y(b,x));
if(c==1||(c==0&&a<b)) return a;
return b;
}
接下来就到了代码中的重点,修改,既然我们已经讲过了操作并模拟了,应该会好理解一点。
先判是否在线段的区间内,若完全覆盖了则与原本这里存比较的谁更优,否则递归左右子树,直到完全覆盖了一些区间。
void modify(int p,int l,int r,int op){
if(tr[p].l>r||tr[p].r<l) return ;
if(tr[p].l>=l&&tr[p].r<=r){//完全覆盖
int mid=(tr[p].l+tr[p].r)/2;
//若新线段在中点更优(判断是否更优)
if(pmx(op,tr[p].bst,mid)==op) swap(op,tr[p].bst);
//若淘汰的在左子树更优,递归左子树
if(pmx(op,tr[p].bst,tr[p].l)==op) modify(p<<1,l,r,op);
//若淘汰的在右子树更优,递归右子树
if(pmx(op,tr[p].bst,tr[p].r)==op) modify(p<<1|1,l,r,op);
return ;
}
//部分覆盖,递归左右子区间(二分)
modify(p<<1,l,r,op);
modify(p<<1|1,l,r,op);
}
查询的就和普通线段树没什么两样了。
若当前的节点是叶子,直接返回最优线段并结束,否则就一直走,到叶子上为止。别忘了要与之前走过的所有线比较谁更优。
int query(int p,int x){
if(tr[p].l==tr[p].r) return tr[p].bst;
int mid=(tr[p].l+tr[p].r)/2;
if(x<=mid) return pmx(tr[p].bst,query(p<<1,x),x);
else return pmx(tr[p].bst,query(p<<1|1,x),x);
}
最后是板子题的代码
$\color{Cyan}Code$
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+10;
struct Line{int x0,y0,x1,y1;}l[N];
struct Segment_T{int l,r,bst;}tr[N<<2];
double _y(int p,int x){//计算x在线段p上的纵坐标
if(l[p].x0==l[p].x1) return max(l[p].y0,l[p].y1);//x0==x1
return 1.0*(x-l[p].x0)*(l[p].y0-l[p].y1)/(l[p].x0-l[p].x1)+l[p].y0;
}
int cmp(double a,double b){//比较哪y个更大,相同则序号小的优先
double ep=1e-9;
if(a-b>ep) return 1;
if(b-a>ep) return -1;
return 0;
}
int pmx(int a,int b,int x){
int c=cmp(_y(a,x),_y(b,x));
if(c==1||(c==0&&a<b)) return a;
return b;
}
void build(int p,int l,int r){
tr[p].l=l,tr[p].r=r;
if(l==r) return ;
int mid=(l+r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
}
void modify(int p,int l,int r,int op){
if(tr[p].l>r||tr[p].r<l) return ;
if(tr[p].l>=l&&tr[p].r<=r){//完全覆盖
int mid=(tr[p].l+tr[p].r)/2;
//若新线段在中点更优(判断是否更优)
if(pmx(op,tr[p].bst,mid)==op) swap(op,tr[p].bst);
//若淘汰的在左子树更优,递归左子树
if(pmx(op,tr[p].bst,tr[p].l)==op) modify(p<<1,l,r,op);
//若淘汰的在右子树更优,递归右子树
if(pmx(op,tr[p].bst,tr[p].r)==op) modify(p<<1|1,l,r,op);
return ;
}
//部分覆盖,递归左右子区间
modify(p<<1,l,r,op);
modify(p<<1|1,l,r,op);
}
int query(int p,int x){
if(tr[p].l==tr[p].r) return tr[p].bst;
int mid=(tr[p].l+tr[p].r)/2;
if(x<=mid) return pmx(tr[p].bst,query(p<<1,x),x);
else return pmx(tr[p].bst,query(p<<1|1,x),x);
}
int n,k,lst,cnt;
int calc1(int x){return (x+lst-1)%39989+1;}
int calc2(int x){return (x+lst-1)%1000000009+1;}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
build(1,1,40000);
for(int i=1,op,x0,x1,y0,y1,k;i<=n;i++){
cin>>op;
if(op==1){
cin>>x0>>y0>>x1>>y1;
x0=calc1(x0),x1=calc1(x1);
y0=calc2(y0),y1=calc2(y1);
if(x0>x1) swap(x0,x1),swap(y0,y1);
l[++cnt]={x0,y0,x1,y1};
modify(1,x0,x1,cnt);
}
else{
cin>>k;
k=calc1(k);
lst=query(1,k);
cout<<lst<<"\n";
}
}
return 0;
}
别忘了在存之后要调用修改函数更新。
然后是dp题。
公式很好推,就是这个(\(dp_i\)表示已经修了前\(i\)个桥的最小花费,\(p_i\)为\(w\)的前缀和,\(h\)就是题面上的):
展开:
我们像斜率优化一样把\(i\)放在前\(j\)放在后:
把等号前的当成\(y\),\(-2\times h_i\times h_j\)的 \(h_j\)当成\(k\),\(h_i\)当成\(x\),因为在当前这个\(x\)时,我们能查到的最优的线就是我们要的\(j\)。
最后就成了这样:
$\color{Cyan}Code$
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10,M=1e6+10;
struct Line{//这里记录的是k,b
int k,b;
Line(){k=0,b=LLONG_MAX;}
}l[N];
struct Segment_T{int l,r,bst;}tr[M<<2];
double _y(int p,int x){return -2*l[p].k*x+l[p].b;}
int cmp(double a,double b){
double ep=1e-9;
if(a-b>ep) return -1;
if(b-a>ep) return 1;
return 0;
}
int pmn(int a,int b,int x){
int c=cmp(_y(a,x),_y(b,x));
if(c==1||(c==0&&a<b)) return a;
return b;
}
void build(int p,int l,int r){
tr[p].l=l,tr[p].r=r;
if(l==r) return ;
int mid=(l+r)>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
}
void modify(int p,int l,int r,int op){
if(tr[p].l>r||tr[p].r<l) return ;
if(tr[p].l>=l&&tr[p].r<=r){
int mid=(tr[p].l+tr[p].r)/2;
if(pmn(op,tr[p].bst,mid)==op) swap(op,tr[p].bst);
if(pmn(op,tr[p].bst,tr[p].l)==op) modify(p<<1,l,r,op);
if(pmn(op,tr[p].bst,tr[p].r)==op) modify(p<<1|1,l,r,op);
return ;
}
modify(p<<1,l,r,op);
modify(p<<1|1,l,r,op);
}
int query(int p,int x){
if(tr[p].l==tr[p].r) return tr[p].bst;
int mid=(tr[p].l+tr[p].r)/2;
if(x<=mid) return pmn(tr[p].bst,query(p<<1,x),x);
else return pmn(tr[p].bst,query(p<<1|1,x),x);
}
int n,h[N],w[N],dp[N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>h[i];
for(int i=1;i<=n;i++) cin>>w[i],w[i]+=w[i-1];
int mx=*max_element(h+1,h+n+1);
build(1,0,mx);
l[1].k=h[1],l[1].b=h[1]*h[1]-w[1];
modify(1,0,mx,1);
for(int i=2;i<=n;i++){
int j=query(1,h[i]);
dp[i]=-2*l[j].k*h[i]+l[j].b+h[i]*h[i]+w[i-1];
l[i].k=h[i];
l[i].b=dp[i]+h[i]*h[i]-w[i];
modify(1,0,mx,i);
}
cout<<dp[n];
return 0;
}
为什么这道题要用李超线段树,不用斜率优化呢?
因为这里的\(b\)不具有单调性,哪怕\(k\)很优,但也无法确定是否是最优的,二分/单调队列就不行了。
顺便安利一下我的斜率优化笔记。

浙公网安备 33010602011771号