李超线段树 学习笔记

李超线段树 学习笔记

引入

最近一直在做斜率优化的题,然而只会傻傻维护凸包,一到横坐标不单调,就涉及到手打平衡树,但是我实在不想学平衡树了,所以就准备掏出解决处理直线的大宝贝——李超线段树

功能

有两种操作:
插入一条表达式为 $ L : y = k\times x+b$ 的直线,给出 \(k,b\)
给出 \(t\),求当前所有直线中与直线 \(x = t\) 交点的纵坐标最大是多少

实现

这里先从“斜率优化”问题中的 “插入的为直线而不是线段” 的简单情况说起。

原理

维护区间的中点对应的最大值的直线,用到了标记永久化的思想。

upd2025.5.7:
所谓标记永久化,就是没有下放操作的线段树,查询的时候访问所有可能对答案产生贡献的节点,也就是单点查询的时候一条路 \(O(\log n)\) 走到底。

维护方法

修改

(偷了两张CSDN博主的图)

image

如当前情况,蓝色的直线是我们加入的直线,如果它比这个区间所有的都大(完全覆盖),那么就把原有线段换成这条直线;反之则直接舍弃。

下一种情况就是不完全覆盖(有交点)

image

  • 首先对于当前这个大区间,现在应该取 \(f(mid )\) 更大的直线作为当前区间的标记,那么对于整个区间都是如此吗?

  • No,可以看到两条直线在 \([l,mid]\) 之间是存在交点的, 那么在左区间的某个位置,\(f(mid')\) 的大小关系就可能发生变化,所以我们需要递归进入有交点的区间来进一步修改

查询

和标记永久化的线段树查询一样,我们查询一个位于 \(x=t\) 的点对应的最大函数值,就要一直从 \([1,n]\) 的直线,一直查询到 \([t,t]\) 的直线,最后取所有可能的最大值,如下图:

image

我们要把橙色、绿色、蓝色、黄色区间的直线之间所有的最大值都取到。

例题

分析

例题用Build Bridges作为板子,本题写出来的状态转移方程是:

\[f_i=min(f_j+(h_i-h_j)^2+sumw_{i-1}-sumw_{j}) \]

按照正常斜率优化的套路写出来的话,原式就变成了:

\[f_i=(h_i^2+s_{i-1})+(-2h_i)h_j+(f_j+h_j^2-sumw_j) \]

这时候你发现我们要作为斜率的\((-2h_i)\)并不单调了,而且插入的点 \((h_i,f_i+h_i^2-sumw_i)\)横坐标也不是单调的,所以这个时候就要把方程换一种写法然后使用李超线段树了:

\[f_i=(h_i^2+s_{i-1})+(-2h_j)h_i+(f_j+h_j^2-sumw_j) \]

可以发现前面是常数项,后面很像一个直线的形式了,我们可以看成给出一个横坐标,求 \(y=(-2h_j)x+(f_j+h_j^2-sumw_j)\)的最小值。

其中我们每给出一条直线,就看成插入 \(k_i=-2h_i,b_i=f_i+h_i^2-sumw_i\)的一条\(y=k_ix+b_i\)的直线,并且在插入之前查询之前存在的所有直线,使得当前的横坐标\(h_i\)对应能取得最小值对应的函数值,然后再插入这条直线。

注意一开始要让\(k[0]=b[0]=INF\),不然第一条直线甚至都无法插入

Code

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define int long long
using namespace std;
const int maxn=1e5+100;
const int maxm=1e6+10;
int n,h[maxn],w[maxn],sumw[maxn],f[maxn];
int k[maxn],b[maxn];
int rt[maxm<<2];
inline int calc(int x/*横坐标*/,int idx/*编号*/){return k[idx]*x+b[idx];}
inline int K(int j){return -2*h[j];}
inline int B(int j){return f[j]+h[j]*h[j]-sumw[j];}
inline int X(int i){return h[i];}
inline void insert(int x,int l,int r,int idx)
{
	if(l==r){if(calc(l,idx)<calc(l,rt[x]))rt[x]=idx;return ;}
	int mid=l+r>>1;
	if(calc(mid,idx)<calc(mid,rt[x]))swap(idx,rt[x]);//完全覆盖 
	if(calc(l,idx)<calc(l,rt[x]))insert(x<<1,l,mid,idx);//左区间可能更新 
	if(calc(r,idx)<calc(r,rt[x]))insert(x<<1|1,mid+1,r,idx);//右区间可能更新 
}
inline int query(int x,int l,int r,int x_given)
{
	if(l==r)return calc(x_given,rt[x]);
	int mid=l+r>>1;
	int nex=x_given<=mid?query(x<<1,l,mid,x_given):query(x<<1|1,mid+1,r,x_given),now=calc(x_given,rt[x]);
	return min(now,nex);//所有包含x_given 的最大值并 
}
void input()
{
	scanf("%lld",&n),b[0]=1e18;
	for(register int i=1;i<=n;++i)scanf("%lld",&h[i]);
	for(register int i=1;i<=n;++i)scanf("%lld",&w[i]),sumw[i]=sumw[i-1]+w[i];
	k[1]=K(1),b[1]=B(1),insert(1,0,maxm,1); 
}
signed main()
{	
	input();
	for(register int i=2;i<=n;++i)
	{
		f[i]=h[i]*h[i]+sumw[i-1]+query(1,0,maxm,X(i));
		k[i]=K(i),b[i]=B(i),insert(1,0,maxm,i);
	}
	printf("%lld",f[n]);
	return 0;
}

注意

写斜率优化的时候完全不用考虑直线的左右端点,直接插入和查询就是了,因为你插入的这个直线一定是无限长的,所以可以把所有的横坐标都囊括,因为我们用当前线段去更新这个区间的前提,就是这个区间被完全包括在这个线段之内。
但是如果插入直线的时候并不是无限长的,我们就要先按照定义找到被完全包含于这条直线的线段区间,然后再对它以及它的子线段递归进行更新。
普通李超线段树的插入写出来就是这样的:
(upd2025.5.7:以下代码是中学时期随便口胡的不保证正确性)

struct SegmentTree{int idx,l,r;}rt[x];
int k[maxn],b[maxn];
int calc(int x,int idx){return 1ll*k[idx]+b[idx];}
inline void modify(int x,int l,int r,int idx)
{
	int mid=rt[x].l+rt[x].r>>1;
	if(l<=rt[x].l&&rt[x].r<=r)
	{
		if(calc(mid,rt[x].idx)>calc(mid,idx))rt[x].idx=idx;
		modify(x<<1,l,r,idx),modify(x<<1|1,l,r,idx);
	}
	if(rt[x].l==rt[x].r)
	{
		if(calc(mid,rt[x].idx)>calc(mid,idx))rt[x].idx=idx;
		return ;
	}
	if(l<=mid)modify(x<<1,l,r,idx);
	if(r>mid)modify(x<<1|1,l,r,idx);
}

以下是大学之后写了一下一般李超线段树之后造出来的代码,这时比插入无限长直线多了一个 \(O(\log n)\) 找区间的过程,所以修改是 \(O(\log{n^2})\) 的复杂度。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
const int INF=1e18;
const double eps=1e-9;
#define double long double
double k[N],b[N];
int cnt;
inline void new_seg(int x,int y,int xx,int yy)
{
    ++cnt;
    if(x==xx)k[cnt]=0,b[cnt]=1.0*max(y,yy);
    else k[cnt]=1.0*(1.0*yy-1.0*y)/(1.0*xx-1.0*x),b[cnt]=1.0*y-k[cnt]*1.0*x;
    // cout<<"NEW:"<<cnt<<' '<<k[cnt]<<' '<<b[cnt]<<'\n';
}
inline double calc(double x,int id){return 1.0*x*k[id]+b[id];}
int tr[N];
inline int cmp(double a,double b)
{
    if(a-b>eps)return 1;
    if(b-a>eps)return -1;
    return 0;
}
inline void modify(int x,int l,int r,int id)
{
    int mid=l+r>>1;
    int ck=cmp(calc(mid,id),calc(mid,tr[x]));
    if(ck==1||(ck==0&&id<tr[x]))swap(tr[x],id);//
    ck=cmp(calc(l,id),calc(l,tr[x]));
    if(ck==1||(ck==0&&id<tr[x]))modify(x<<1,l,mid,id);//
    ck=cmp(calc(r,id),calc(r,tr[x]));
    if(ck==1||(ck==0&&id<tr[x]))modify(x<<1|1,mid+1,r,id);//
}
//相等时,但是序号相对来说更小 也需要更新,特别注意浮点数特殊的大小比较
inline void position(int x,int l,int r,int modl,int modr,int id)
{
    if(modl<=l&&r<=modr)return modify(x,l,r,id),void();
    int mid=l+r>>1;
    if(modl<=mid)position(x<<1,l,mid,modl,modr,id);
    if(modr>mid)position(x<<1|1,mid+1,r,modl,modr,id);
}
inline pair<int,double>max_pair(pair<int,double> a,pair<int,double> b)
{
    int ck=cmp(a.second,b.second);
    if(ck==1)return a;
    else if(ck==-1)return b;
    else return a.first<b.first?a:b;
}
inline pair<int,double> query(int x,int l,int r,int pos)
{
    if(l==pos&&r==pos)return make_pair(tr[x],calc(pos,tr[x]));
    int mid=l+r>>1;
    pair<int,double> ans=make_pair(tr[x],calc(pos,tr[x]));
    if(pos<=mid)ans=max_pair(ans,query(x<<1,l,mid,pos));
    else ans=max_pair(ans,query(x<<1|1,mid+1,r,pos));
    return ans;
}
const int MOD1=39989,MOD2=1e9;
int main()
{
    b[0]=-INF;
    ios::sync_with_stdio(0);
    cin.tie(0);
    int q,opt,pos;
    cin>>q;
    int lastans=0;
    while(q--)
    {
        cin>>opt;
        if(opt==0)
        {
            cin>>pos;
            pos=(pos+lastans-1)%MOD1+1;
            lastans=query(1,1,40000,pos).first;
            cout<<lastans<<'\n';
        }
        else
        {
            int x,xx,y,yy;
            cin>>x>>y>>xx>>yy;
            x=(x+lastans-1)%MOD1+1;
            xx=(xx+lastans-1)%MOD1+1;
            y=(y+lastans-1)%MOD2+1;
            yy=(yy+lastans-1)%MOD2+1;
            new_seg(x,y,xx,yy);
            if(x>xx)swap(x,xx);
            position(1,1,40000,x,xx,cnt);
        }
    }
    return 0;
}

复杂度

综上,斜率优化写的李超树只是李超线段树的一种特殊情况(每条直线的端点就都是整个区间的左右端点)。
我们在写普通李超线段树的插入操作的时候,把区间分割成一个个能被完全包含的小区间的复杂度是 \(O(\log n)\) 的,然后对于每一个这样的区间,我们还要分别对其进行一次 \(O(\log n)\) 的更新,所以总的复杂度是 \(O(\log^2n)\) 的,而询问的就是对于所在的所有区间的最大值/最小值取并集,所以就是 \(O(\log n)\) 的。
插入:\(O(\log^2n)\)
查询:\(O(\log n)\)

posted @ 2022-10-26 19:43  Hanggoash  阅读(51)  评论(0)    收藏  举报
动态线条
动态线条end