介绍
李超线段树,是用来解决平面直角坐标系中直线或线段的集合在某一点 x x x处的最大值或最小值问题。
在实现李超线段树的时候,打的标记是不用下传的,也就是标记永久化。在查询时,根节点到对应叶节点上的所有节点的贡献都要计算。
线段树的区间对应的是平面直角坐标系的 x x x轴上的区间,对于每个区间,要维护的是在这个区间上较优的直线或线段。
维护线段
在线段树上的操作如下:
- 修改操作:加入一条线段
- 查询操作:查询所有包含 x = x 0 x=x_0 x=x0的线段在 x = x 0 x=x_0 x=x0时能取到的最大值
下面来分类讨论一下每种情况。
- 如果线段覆盖的这个区间与当前区间并不相交,则直接返回
- 如果线段覆盖了这个区间的一部分,则递归对应的左右子区间继续处理
- 如果线段覆盖了整个区间
- 如果线段在区间两端点处的值都比原来区间大,则用当前线段替换原来的线段并返回
- 如果线段在区间两端点处的值都比原来区间小,则直接返回
- 否则,比较当前线段和原来线段在区间中点处的值,如果当前线段值更大,则将原来线段和当前线段交换;然后判断左右端点的值,如果当前线段在一边不占优势,则递归对应左右子区间
修改时要先找到修改分配到 log n \log n logn个线段树上的区间,这些区间需要 O ( log n ) O(\log n) O(logn)的时间复杂度下传,所以单次修改的时间复杂度为 O ( log 2 n ) O(\log^2 n) O(log2n)。同样地,查询也是 O ( log 2 n ) O(\log^2 n) O(log2n)。
例题
P4097 【模板】李超线段树 / [HEOI2013] Segment
模板题,按上面说的做即可。时间复杂度为 O ( n log 2 n ) O(n\log^2 n) O(nlog2n)。
code
#include<bits/stdc++.h>
using namespace std;
const double eps=1e-8;
const long long mod1=39989,mod2=1000000001;
int n,s1=0,cnt=0,rt=0,ans=0;
struct node{
double k,b;
}s[100005];
struct tree{
int lc,rc,c;
}tr[500005];
bool pd(int od,int wd,int x){
double y1=s[od].k*x+s[od].b;
double y2=s[wd].k*x+s[wd].b;
if(abs(y1-y2)<eps) return od>wd;
return y1<y2;
}
void insert(int &k,int l,int r,int x,int y,int c){
if(!k) k=++cnt;
if(l>=x&&r<=y){
if(pd(tr[k].c,c,l)&&pd(tr[k].c,c,r)){
tr[k].c=c;return;
}
if(pd(c,tr[k].c,l)&&pd(c,tr[k].c,r)) return;
int mid=l+r>>1;
if(pd(tr[k].c,c,mid)) swap(tr[k].c,c);
if(pd(tr[k].c,c,l)) insert(tr[k].lc,l,mid,x,y,c);
else insert(tr[k].rc,mid+1,r,x,y,c);
return;
}
if(l>y||r<x) return;
if(l==r) return;
int mid=l+r>>1;
if(x<=mid) insert(tr[k].lc,l,mid,x,y,c);
if(y>mid) insert(tr[k].rc,mid+1,r,x,y,c);
}
int find(int k,int l,int r,int x){
if(l==r&&l==x) return tr[k].c;
int mid=l+r>>1,re=0;
if(x<=mid) re=find(tr[k].lc,l,mid,x);
else re=find(tr[k].rc,mid+1,r,x);
if(pd(re,tr[k].c,x)) re=tr[k].c;
return re;
}
int main()
{
scanf("%d",&n);
for(int i=1,op,v1,v2,w1,w2;i<=n;i++){
scanf("%d",&op);
if(op==1){
scanf("%d%d%d%d",&v1,&w1,&v2,&w2);
v1=(v1+ans-1)%mod1+1;
v2=(v2+ans-1)%mod1+1;
w1=(w1+ans-1)%mod2+1;
w2=(w2+ans-1)%mod2+1;
if(v1>v2){
swap(v1,v2);swap(w1,w2);
}
if(v1==v2){
s[++s1]=(node){0,max(w1,w2)};
}
else{
double k=(w2-w1)*1.0/(v2-v1);
s[++s1]=(node){k,w1-v1*k};
}
insert(rt,1,40000,v1,v2,s1);
}
else{
scanf("%d",&v1);
v1=(v1+ans-1)%mod1+1;
ans=find(rt,1,40000,v1);
printf("%d\n",ans);
}
}
return 0;
}
维护直线
与维护线段的方法类似,还要简单一些。不用考虑线段没有覆盖整个区间的情况,再按维护线段的方法实现即可。
修改时直线只分配到 1 1 1个线段树上的区间(就是整个线段树维护的区间),这个区间需要 O ( log n ) O(\log n) O(logn)的时间复杂度下传,所以单次修改的时间复杂度为 O ( log n ) O(\log n) O(logn)。同样地,查询也是 O ( log n ) O(\log n) O(logn)。
例题
P4254 [JSOI2008] Blue Mary 开公司
也是一道模板题,在维护线段的代码的基础上删掉线段没有覆盖整个区间的情况即可。时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)。
code
#include<bits/stdc++.h>
using namespace std;
const double eps=1e-8;
int n,s1,cnt=0,rt,ans;
char ch[15];
struct node{
double k,b;
}s[100005];
struct tree{
int lc,rc,c;
}tr[500005];
bool pd(int od,int wd,int x){
double w1=s[od].k+x*s[od].b;
double w2=s[wd].k+x*s[wd].b;
if(abs(w1-w2)<eps) return od>wd;
return w1<w2;
}
void insert(int &k,int l,int r,int c){
if(!k) k=++cnt;
if(pd(tr[k].c,c,l)&&pd(tr[k].c,c,r)){
swap(tr[k].c,c);return;
}
if(pd(c,tr[k].c,l)&&pd(c,tr[k].c,r)) return;
int mid=l+r>>1;
if(pd(tr[k].c,c,mid)) swap(tr[k].c,c);
if(pd(tr[k].c,c,l)) insert(tr[k].lc,l,mid,c);
else insert(tr[k].rc,mid+1,r,c);
}
int find(int k,int l,int r,int x){
if(l==r&&l==x) return tr[k].c;
int mid=l+r>>1,re=0;
if(x<=mid) re=find(tr[k].lc,l,mid,x);
else re=find(tr[k].rc,mid+1,r,x);
if(pd(re,tr[k].c,x)) re=tr[k].c;
return re;
}
int main()
{
scanf("%d",&n);
s[0]=(node){0,-1e9};
for(int i=1,x;i<=n;i++){
scanf("%s",ch);
if(ch[0]=='P'){
double k,b;
scanf("%lf%lf",&k,&b);
s[++s1]=(node){k-b,b};
insert(rt,1,50000,s1);
}
else{
scanf("%d",&x);
ans=find(rt,1,50000,x);
ans=s[ans].k+x*s[ans].b;
if(ans<0) ans=0;
printf("%d\n",ans/100);
}
}
return 0;
}