李超线段树学习笔记
前言
如有错误,欢迎各位大佬指出。
GM说学了斜率和线段树就可以尝试。
前置芝士:
-
斜率
-
线段树
1.什么是李超线段树?
李超线段树主要解决平面坐标系内有关直线的问题,李超线段树是一种特殊的线段树。
这里给出一个引例 P4097 [HEOI2013]Segment。
题目大意及要维护两个操作:
-
给定一条线段的左端点和右端点。
-
给定条直线 \(x=k\),求与该直线相交的线段的最大 \(y\) 值是多少。
对于这个题,如果要用普通线段树维护,那么我们就必须采用权值线段树,而且很难讲左右子树进行合并,并不方便。因此,这个题就需要用到李超线段树了。
2.李超线段树的具体做法。
显然,对于这道题,无论如何都是要采用权值线段树的。但是我们在维护的时候,可以有一些不同的做法。
首先,我们以 \(x\) 轴为权值,维护一颗权值线段树,对于每一个节点,记录该节点上,\(y\) 值最大的线段的编号。
然后,我们在修改的时候,找到线段的定义域所完全覆盖的所有区间,然后拿出来进行单独维护。
对于单独维护的区间 \([l,r]\),我们找到中点 \(mid\),假设新加入的线段编号为 \(y\)。
如果当前区间并没有最大的线段,则将其设为 \(y\)。
接着,比较 \(x=mid\) 时,比较该区间原来最大的线段和新加入的线段的大小,如果新的线段更大,则将原来的编号和现在的编号直接交换。(可以发现无论如何,交不交换,现在编号都是与现在维护的节点最大值不同的那一个)
接着,我们就要看不能成为该区间最大值的编号(即现在的编号)可不可能成为该区间子节点的最大值。
如果在 \(x=l\)(\(l\) 为区间左端点)时,现在的编号得到的值大于原来的值,则显然该区间节点的左子树的左右端点的最大值都由现在的编号产生,所以现在的编号就能成为左子树的最大值,就向下递归。
当判断需不需要递归右子树时也同理。
最后注意两个细节。
-
当 \(y\) 值相同时,是求编号最小的线段,所以在判断需不需要递归的时候,要判断 \(y\) 值相等时的情况。
-
在求线段解析式时,如果 \(x_1=x_2\) 要特判。
代码
#include<bits/stdc++.h>
using namespace std;
int n;
int op,k;
int x[100005],y[100005],x2[100005],y2[100005];
struct line
{
double k,b;
}arr[100005];
double calc(int i,int x)
{
return arr[i].k*(double)x+arr[i].b;
}
int tot;
void adds(int i)
{
++tot;
if(x[i]==x2[i]) arr[tot].k=0,arr[tot].b=max(y[i],y2[i]);
else arr[tot].k=1.0*(y2[i]-y[i])/(x2[i]-x[i]),arr[tot].b=y[i]-arr[tot].k*x[i];
}
struct Li_chao_tree
{
int l,r;
int id;
}tree[200005];
int cnt;
void build(int p,int l,int r)
{
tree[p].l=l,tree[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 update(int p,int x)
{
int mid=(tree[p].l+tree[p].r)>>1;
if(calc(x,mid)>calc(tree[p].id,mid)) swap(tree[p].id,x);
int y=tree[p].id;
if(calc(x,tree[p].l)>calc(y,tree[p].l)||(calc(x,tree[p].l)==calc(y,tree[p].l)&&x<y)) update(p<<1,x);
if(calc(x,tree[p].r)>calc(y,tree[p].r)||(calc(x,tree[p].r)==calc(y,tree[p].r)&&x<y)) update(p<<1|1,x);
}
void change(int p,int l,int r,int x)
{
if(tree[p].r<l||tree[p].l>r) return;
if(tree[p].l>=l&&tree[p].r<=r)
{
update(p,x);
return;
}
change(p<<1,l,r,x);
change(p<<1|1,l,r,x);
}
pair<double,int> pmax(pair<double,int> a,pair<double,int> b)
{
if(fabs(a.first-b.first)<1e-9) return min(a,b);
else return max(a,b);
}
pair<double,int> query(int p,int x)
{
if(tree[p].l>x||tree[p].r<x) return make_pair(0,0);
if(tree[p].l==tree[p].r) return make_pair(calc(tree[p].id,x),tree[p].id);
int y=tree[p].id;
pair<double,int> now;
if(y) now=make_pair(calc(tree[p].id,x),tree[p].id);
else now=make_pair(0,0);
return pmax(now,pmax(query(p<<1,x),query(p<<1|1,x)));
}
signed main()
{
scanf("%d",&n);
int lastans=0;
build(1,1,39989);
for(int i=1;i<=n;++i)
{
int op;
scanf("%d",&op);
if(op==0)
{
scanf("%d",&k);
k=(k+lastans-1+39989)%39989+1;
printf("%d\n",lastans=query(1,k).second);
}
else
{
scanf("%d%d%d%d",&x[i],&y[i],&x2[i],&y2[i]);
x[i]=(x[i]+lastans-1+39989)%39989+1;
y[i]=(y[i]+lastans-1+(int)1e9)%(int)(1e9)+1;
x2[i]=(x2[i]+lastans-1+39989)%39989+1;
y2[i]=(y2[i]+lastans-1+(int)1e9)%(int)(1e9)+1;
adds(i);
change(1,min(x[i],x2[i]),max(x[i],x2[i]),tot);
}
}
}
3.李超线段树的时间复杂度
首先,对于建树和查询操作,都是标准的线段树,单次操作时间复杂度都为 \(log2(n)\)。
然后,对于修改操作,由于我们第一步需要找到给区间完全覆盖的所有节点,需要 \(log2(n)\) 然后还需要在每个区间进行更新答案,又需要 \(log2(n)\) 的时间,所以修改操作的时间就为 \(log2(n)^2\)。
4.李超线段树的应用
-
在引入中有说可以解决平面坐标系内的问题
-
仔细思考可以发现,斜率优化是要维护截距的最大或最小,让我们就可以把所有的直线放进去,然后看在 \(x=0\) 时,\(y\) 值得最大最小的编号是多少,还是很板的。唯一的缺点就是时间复杂度多了 \(log2(n)\),并且码长变大了,不过倒是解决了分不清楚小于大于符号已及二分的问题。
相关例题
- 普通李超线段树:
- 斜率优化: