李超线段树学习笔记
数据结构核心
李超线段树主要作用是维护区间上的线段,操作有插入一条线段和查询某个点上最值。
每个节点上我们村的是当前区间的中间点最优的情况,这里用到了标记永久化思想(毕竟一次函数是变的,当前区间最优不一定是这个区间子区间的最优)。
对于插入操作,不妨设区间中点处新的更优,那么如果左端点新的反而比旧的劣,说明新旧两条线在左侧有交点,那就再去更新左侧,同理,如果右端点新的反而比旧的劣,更新右侧。
对于查询操作,注意因为有标记永久化,所以每一层都要取一个最优。
例题
P4097板子题
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#define y1 Y1
#define M 40000
#define D double
#define ls(p) (p<<1)
#define rs(p) (p<<1|1)
using namespace std;
const int Mod1=39989;
const int Mod2=1e9;
const D eps=1e-8;
int cnt,lastans;
struct Line{
int id;D k,b;
Line(){}
Line(D x0,D y0,D x1,D y1,int _id){
id=_id;
if(abs(x0-x1)<eps)k=0,b=max(y0,y1);
else
{
k=(y1-y0)/(x1-x0);
b=y0-k*x0;
}
}
D val(D x){return k*x+b;}
};Line t[M<<2];
void update(int p,int pl,int pr,int L,int R,Line g)
{
if(L==pl&&pr==R)
{
if(t[p].id==0){t[p]=g;return;}
int mid=(pl+pr)>>1;
if(abs(t[p].val(mid)-g.val(mid))<eps)
{
if(t[p].id<g.id)swap(t[p],g);
}
else if(t[p].val(mid)<g.val(mid))swap(t[p],g);
int fl=(g.val(pl)+eps<t[p].val(pl));
int fr=(g.val(pr)+eps<t[p].val(pr));
if(fl&&fr)return;
if(!fl)update(ls(p),pl,mid,L,mid,g);
if(!fr)update(rs(p),mid+1,pr,mid+1,R,g);
return;
}
int mid=(pl+pr)>>1;
if(R<=mid)update(ls(p),pl,mid,L,R,g);
else if(L>mid)update(rs(p),mid+1,pr,L,R,g);
else update(ls(p),pl,mid,L,mid,g),update(rs(p),mid+1,pr,mid+1,R,g);
}
Line query(int p,int pl,int pr,int pos)
{
if(pl==pr)return t[p];
int mid=(pl+pr)>>1;Line ret;
if(pos<=mid)ret=query(ls(p),pl,mid,pos);
else ret=query(rs(p),mid+1,pr,pos);
if(abs(ret.val(pos)-t[p].val(pos))<eps)
{
if(ret.id<t[p].id)return ret;
return t[p];
}
else
{
if(ret.val(pos)>t[p].val(pos))return ret;
else return t[p];
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n;cin>>n;
while(n--)
{
int op;cin>>op;
if(!op)
{
int k;cin>>k;
k=(k+lastans-1+Mod1)%Mod1+1;
Line ret=query(1,1,M,k);
lastans=ret.id;
cout<<lastans<<"\n";
}
else
{
int x0,y0,x1,y1;
cin>>x0>>y0>>x1>>y1;
x0=(x0+lastans-1+Mod1)%Mod1+1;
y0=(y0+lastans-1+Mod2)%Mod2+1;
x1=(x1+lastans-1+Mod1)%Mod1+1;
y1=(y1+lastans-1+Mod2)%Mod2+1;
if(x0>x1)swap(x0,x1),swap(y0,y1);
++cnt;Line G=Line(x0,y0,x1,y1,cnt);
update(1,1,M,x0,x1,G);
}
}
return 0;
}
拓展
AT_dp_z 李超线段树在DP上的应用
是个斜率优化的板子,但是这道题我选择用李超做。
设 \(f_i\) 表示到第 \(i\) 个点的最短距离,可得转移:\( f_i=min\{f_j+(h_j-h_i)^2\}+C \)。
暴力拆开,得到 \(f_i=min\{f_j+h_j^2-2h_jh_i+h_i^2\}+C\)。
把跟 \(j\) 无关的挪到大括号外面,得到 \(f_i=min\{-2h_j \cdot h_i+f_j+h_j^2\}+h_i^2+C\)。
把 \(h_i\) 看做自变量,则 \(-2h_j \cdot h_i+f_j+h_j^2\) 就是个一次函数,可以用李超线段树维护最小值。
代码:
struct line{
int k,b,id;
line(){k=0,b=1e18,id=0;}
line(int _k,int _b,int _id){k=_k,b=_b,id=_id;}
int f(int x){return k*x+b;}
}t[4000005];
void update(int p,int pl,int pr,int L,int R,line g)
{
if(L<=pl&&pr<=R)
{
if(!t[p].id){t[p]=g;return;}
int mid=(pl+pr)>>1;
if(g.f(mid)<t[p].f(mid))swap(t[p],g);
if(t[p].f(pl)>g.f(pl))update(p*2,pl,mid,L,R,g);
if(t[p].f(pr)>g.f(pr))update(p*2+1,mid+1,pr,L,R,g);
return;
}
int mid=(pl+pr)>>1;
if(L<=mid)update(p*2,pl,mid,L,R,g);
if(R>mid)update(p*2+1,mid+1,pr,L,R,g);
}
int query(int p,int pl,int pr,int k)
{
if(pl==pr){return t[p].f(k);}
int mid=(pl+pr)>>1;
if(k<=mid)return min(t[p].f(k),query(p*2,pl,mid,k));
else return min(t[p].f(k),query(p*2+1,mid+1,pr,k));
}
int cnt,f[200005],n,C,h[200005],m;
signed main()
{
scanf("%lld%lld",&n,&C);
for(int i=1;i<=n;i++)scanf("%lld",&h[i]),m=max(m,h[i]);
++cnt;line g=line(-2*h[1],h[1]*h[1],cnt);
update(1,1,m,1,m,g);
for(int i=2;i<=n;i++)
{
f[i]=query(1,1,m,h[i])+h[i]*h[i]+C;
++cnt;line g=line(-2*h[i],h[i]*h[i]+f[i],cnt);
update(1,1,m,1,m,g);
}
printf("%lld\n",f[n]);
return 0;
}
upd.2024/9/3 在被 HDU3507 创飞之后痛定思痛,意识到还是得看数据范围。
P4072 推式子
不妨设每一段的路程分别为 \(v_1,v_2,\dots,v_m\),设 $S_i\sum_{j=1}^i a_j $,则我们有:
则
我们设 \(f_{i,j}\) 表示前 \(i\) 段路程休息 \(j\) 次的最小代价,则 \(f_{i,l}=\min\{f_{j,l-1}+(s_i-s_j)^2\}=\min\{-2s_j\cdot s_i+s_j^2+f_{j,l-1}\}+s_i^2\),李超线段树维护就完辣!
放代码吗?不放了,放就炸了。
然后,我做到了P2120,然后学习了动态开点李超线段树。
动态开点李超线段树
=李超线段树+动态开点,会线段树的动态开点就会这个的动态开点。一些需要注意的细节写在注释里了。代码(以P2120 为例):
struct line{
int k,b;
line(){k=0,b=INF;}
line(int _k,int _b){k=_k,b=_b;}
int f(int x){return k*x+b;}
};
struct node{
int ls,rs; line l;
int f(int x){return l.k*x+l.b;}
//为什么这里要在节点内把 line 分开?
//因为 update 里的 swap 操作如果直接交换 tr[p] 和 g 会出错。
}tr[N];
int nc,rt;
void update(int &p,int pl,int pr,line g)
{
if(!p){tr[p=++nc].l=g;return;}
int mid=(pl+pr)>>1;
if(tr[p].f(mid)>g.f(mid))swap(tr[p].l,g);
if(tr[p].l.k<g.k)update(tr[p].ls,pl,mid,g);
else update(tr[p].rs,mid+1,pr,g);
}
int query(int p,int pl,int pr,int k)
{
if(!p)return INF;
int mid=(pl+pr)>>1;
if(k<=mid)return min(tr[p].f(k),query(tr[p].ls,pl,mid,k));
else return min(tr[p].f(k),query(tr[p].rs,mid+1,pr,k));
}
然后,我就可以自信地喊出学动态开点李超线段树之前不敢喊出来的那句话:
一切斜率优化都可以用李超线段树草过去!
附录
“集训”合集中关于李超 seg 的文章:2024/1/11