李超线段树是一种用于维护平面直角坐标系内线段关系的数据结构,插入直线/线段,支持查询单点极值
李超树的经典应用是斜率优化,可以看下这篇文章
李超线段树没有用懒标记实现区间修改,而用的是标记永久化
其实标记永久化与我们对lazy标记的理解非常相同,可以看看LYD蓝书上对标记永久化的解释,都是累积某个节点到根节点路径上的所有点的影响
具体来说,我们用\(s[p]\)表示线段树上\(p\)节点存储线段的编号,每次的修改的区间就是\([x_0,x_1]\)
任意一个时刻,线段树上的任意一个节点的\(s[p]\)的值是唯一的,对于一个叶子节点,他的真实线段编号(假设这个叶子节点的横坐标是\(x_0\),我们做一条直线\(x=x_0\),与所有已经插入的线段的交点纵坐标最大且在此前提下线段标号最小的线段的编号)就是从这个叶子节点到根节点的路径上所有节点存储的线段进行计算比较得出来的那一条线段
那么我们怎么修改呢?标记永久化是不用下传的,想一下,如果一个节点没有被\([x_0,x_1]\)完全包含,只是相交,那么我们不动这个节点的\(s[p]\),对最终的答案是没有影响的,任意一个叶子节点往上走所得到的真实线段一定是不会被遗漏的
所以我们只用一直递归,直到某个节点被修改区间完全包含为止
此时,我们怎么修改呢?
如果这个区间还没有线段,那么直接令这个区间的\(s[p]\)为当前这个修改区间的编号并且返回即可
如果这个区间已经有一个线段了,比如
其中\(f\)是新插入的线段,\(g\)是原来的线段
就如我们图像所看到的,我们很明显的应该保存形状为“V”的折线段(实际中的线段树上的点是离散的,这个不影响),所以左边就可以直接赋值,然后右边往下递归修改就好了
具体来说,设当前区间的中点为\(m\),我们拿新线段\(f\)在中点处的值与原最优线段\(g\)在中点处的值作比较。
如果新线段\(f\)更优,则将\(f\)和\(g\)交换(这个操作的正确性在后文讲述)。那么现在考虑在中点处\(f\)不如\(g\)优的情况:
-
若在左端点处\(f\)更优,那么\(f\)和\(g\)必然在左半区间中产生了交点,\(f\)只有在左区间才可能优于\(g\),递归到左儿子中进行下传;
-
若在右端点处\(f\)更优,那么\(f\)和\(g\)必然在右半区间中产生了交点,\(f\)只有在右区间才可能优于\(g\),递归到右儿子中进行下传;
-
若在左右端点处\(g\)都更优,那么\(f\)不可能成为答案,不需要继续下传。
-
\(f\)和\(g\)刚好交于中点,在程序实现时可以归入中点处\(f\)不如\(g\)优的情况,结果会往\(f\)更优的一个端点进行递归下传。
最后令\(s[p]=g\)的编号即可
参考代码:
void modify(int p,int l,int r,int x,int y,int u)
{
if(l>y||r<x) return;
if(x<=l&&y>=r)
{
update(p,l,r,u);
return;
}
int mid=l+r>>1;
//标记永久化不下放标记
//直接递归
modify(p<<1,l,mid,x,y,u);
modify(p<<1|1,mid+1,r,x,y,u);
}
double calc(int id,int d)
{
return q[id].b+q[id].k*d;
}
int cmp(double x, double y)
{
if(x-y>eps) return 1;
if(y-x>eps) return -1;
return 0;
}
void update(int p,int l,int r,int u)
{
int &v=s[p],mid=l+r>>1;
int bmid=cmp(calc(u,mid),calc(v,mid));//判断f和g的中点值大小
if(bmid==1||(!bmid&&u<v)) swap(u,v);
//这里如果中点相等
//我们交换反正不会遗漏纵坐标的最优值
//而且还可能使得编号更优
//就是白换白不换
int bl=cmp(calc(u,l),calc(v,l)),br=cmp(calc(u,r),calc(v,r));
if(bl==1||(!bl&&u<v)) update(p<<1,l,mid, u);
if(br==1||(!br&&u<v)) update(p<<1|1,mid+1,r,u);
//其实这两个判断||后面的这个判断条件是需要的
//在端点处有可能会因为端点更优而更新
//但也可能导致一个问题
//如果修改的这条线段与原来的某个线段完全重合
//就会递归两个子树
//复杂度就会退化了
}
不难看出时间复杂度是\(O(log^2n)\)
那么为什么最开始交换\(f\)和\(g\)是正确的呢?这就要考虑我们的标记永久化的思想了,因为标记永久化是累积路径上所有节点的影响,你把当前节点的已经标记的线段和即将标记的线段交换一下,并利用交换后的即将标记的线段往下面传递进行修改,在修改完之后,任何一个叶子节点的真实线段编号是不会受影响的(比如就认为当前这个区间有两条线段\(f\)和\(g\),然后\(f\)和\(g\)对这个区间所管辖的所有叶子节点的真实线段的影响就是他们两个谁更大,不用递归的那半边就是更高的那个线段,用递归的那半边就继续递归讨论就好了)
查询参考代码:
pdi pmax(pdi x,pdi y)
{
if(cmp(x.first,y.first)==-1)
return y;
else if(cmp(x.first,y.first)==1)
return x;
else//如果纵坐标相同,返回编号更小的一个点
return x.second<y.second?x:y;
}
pdi ask(int p,int l,int r,int d)
{
if(r<d||l>d) return {0,0};
int mid=l+r>>1;
double res=calc(s[p],d);
if(l==r) return {res,s[p]};
//根据标记永久化思想
//我们要累计回根路径上所有点的影响
return pmax({res,s[p]},pmax(ask(p<<1,l,mid,d),ask(p<<1|1,mid+1,r,d)));
}
注意是标记永久化
完整代码见洛谷