李超线段树

写在前面

一个博主很早之前学的算法,只记录了自己的模板没写总结,导致在一场考试中,遇到原题但只能含泪写暴力,所以先趁热打铁来写一个博客。

简介

李超线段树,可能才是真正的线段树,因为它真的在记录线段。
用途是来解决一些插入直线/线段,支持查询单点极值的问题。

前置知识

线段树(不会就不用学了)和标记永久化(很简单),关于直线的一些计算(小学生都会)

思路

对于每个线段树节点表示的区间是 \([l,r]\) 则线段树上记录的就是区间中点上的极值的线段标号。(你可能觉得重复记录了一些值,但你发现它非常有用)。
对于一个线段的插入,我们发现不能直接插入线段,因为发现可能会更新它不存在的区间。所以我们需要先找出被它完全覆盖的区间(线段树查询一样的做法),然后就可以直接加入线段了。
对于被它完全覆盖的区间,我们要分讨。

  1. 如果当前节点没有线段,就直接记录下来,然后返回。
  2. 1不对,就与线段树上的线段比较在中点位置上的值,选择更优的一个留下,另一个进入三。(这样做,你会发现定义不一定正确,最后留在节点上的不一定最优,因为最优的可能每传下去)
  3. 此时我们保证了中点处线段树上的是极值,所以考虑另一个线段可能造成贡献的区间。如果左端点低,则说明在左区间有交点,向左区间递归。如果右端点低,则说明在右区间有交点,向右区间递归。如果被完全碾压,就退出。

然后我们考虑如何查询,对于每个点我们都遍历到最底层的节点,取过程中遇到的每一个线段在目标位置上的值中的极值。这里我们就能理解插入为什么是对的。
然后时间复杂度,我们发现插入线段的复杂度是 \(O(\log^2 n)\) (如果是直线就只有一个 \(\log\) ),然后询问是 \(O(\log n)\) 的。

代码

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10,M=39989,inf=1e9+10;
const double eps=1e-9;
int lasans,cnt,n,sh[N];
struct stu
{
    double k,b;
}q[N];
struct node
{
    double zhi;int id;
};
double calc(int x,int wz){return q[x].b+q[x].k*wz;}
int cmp(double zhix,double zhik)
{
    if(zhix-zhik>eps)return 1;
    if(zhik-zhix>eps)return 2;//精度
    return 0;
}
void upd(int x,int l,int r,int k)
{
    int mid=(l+r)>>1;
    int fmid=cmp(calc(sh[x],mid),calc(k,mid));
    if(fmid==2||(fmid==0&&sh[x]>k))swap(sh[x],k);
    int fl=cmp(calc(sh[x],l),calc(k,l)),fr=cmp(calc(sh[x],r),calc(k,r));
    if(fl==2||(fl==0&&sh[x]>k))upd(x<<1,l,mid,k);
    if(fr==2||(fr==0&&sh[x]>k))upd(x<<1|1,mid+1,r,k);
    return ;
}
void update(int x,int l,int r,int lt,int rt,int k)
{
	if(lt<=l&&r<=rt)
	{
		upd(x,l,r,k);//完全覆盖的区间直接加入
		return;
	}
	int mid=(l+r)>>1;
	if(lt<=mid)update(x<<1,l,mid,lt,rt,k);
	if(rt>mid)update(x<<1|1,mid+1,r,lt,rt,k);
	return ;
}
void add(int x0,int y0,int x1,int y1)//处理出线段信息
{
    if(x0==x1){q[++cnt].k=0;q[cnt].b=max(y0,y1);}//处理与y轴平行的线段
    else{q[++cnt].k=1.0*(y1-y0)/(x1-x0);q[cnt].b=y0-q[cnt].k*x0;}
}
node nmax(node x,node y)
{
    int f=cmp(x.zhi,y.zhi);
    if(f==1)return x;
    if(f==2)return y;
    else return x.id<y.id?x:y;
}
node query(int x,int l,int r,int k)
{
    if(r<k||l>k)return {0,0};//不在区间内
    int mid=(l+r)>>1;
    double zhi=calc(sh[x],k);//是k不是mid
    if(l==r)return {zhi,sh[x]};
    return nmax({zhi,sh[x]},nmax(query(x<<1,l,mid,k),query(x<<1|1,mid+1,r,k)));
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
    cin>>n;
    q[0]={0,-inf};//注意需要先初始0
    for(int i=1;i<=n;i++)
    {
        int op;
        cin>>op;
        if(op==1)
        {
            int x0,y0,x1,y1;
            cin>>x0>>y0>>x1>>y1;
            x0=(x0+lasans-1)%39989+1;x1=(x1+lasans-1)%39989+1;
            y0=(y0+lasans-1)%1000000000+1;y1=(y1+lasans-1)%1000000000+1;//强制在线
            if(x0>x1)swap(x0,x1),swap(y0,y1);//保证左右端点
            add(x0,y0,x1,y1);//增加一个线段。
            update(1,1,M,x0,x1,cnt);
        }
        else
        {
            int x;cin>>x;
            x=(x+lasans-1)%39989+1;
            cout<<(lasans=query(1,1,M,x).id)<<'\n';
        }
    }
	return 0;
} 

易错点

  1. 要有初始的哨兵(初始化0号的值)
  2. 有些题要注意x1,x2的大小关系
  3. 使用可能需要考虑精度问题

例题

3.15考试题

大概就是插入线段,求单点最小值。
与模板题基本一致,改一下极值判断即可。同时因为值域为 \(1e9\) 所以请使用动态开点线段树。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10,inf=1e9;
const double eps=1e-8;
struct  node
{
	int x1,y1,x2,y2;
	double k;
}f[N];
struct stu
{
	int db,ls,rs;
}sh[N*31];//开够了吗 
int m,cnt,tot,rt;//不要混用 
double js(int j,int a)
{
//	cout<<j<<" "<<a<<" "<<f[j].y1<<" "<<f[j].k*(a-f[j].x1)<<'\n';
	return 1.0*f[j].y1+f[j].k*(a-f[j].x1);
}
double query(int x,int l,int r,int wz)
{
	if(x==0)return inf+10;
	int mid=(l+r)>>1;double res=inf+10;
	//cout<<x<<" "<<l<<" "<<r<<'\n';
	if(sh[x].db)
	{
	//	cout<<x<<" "<<l<<" "<<r<<" "<<sh[x].db<<'\n';
		res=js(sh[x].db,wz);//想清楚求得是什么 
	}	 
	if(l==r)return res;
	if(wz<=mid)return min(res,query(sh[x].ls,l,mid,wz));
	else return min(res,query(sh[x].rs,mid+1,r,wz));
}
int check(int wz,int u,int v)
{
	double zu=js(u,wz),zv=js(v,wz);
	if(zv-zu>eps)return 2;//注意精度问题 
	if(zu-zv>eps)return 1;
	return 0;
} 
void insert(int& x,int l,int r,int z)
{
	//cout<<l<<' '<<r<<'\n'; 
	if(!x)x=++tot;
	if(!sh[x].db)
	{
		//cout<<x<<" "<<l<<" "<<r<<'\n';
		sh[x].db=z;return;
	}//不存在结束 
	int mid=(l+r)>>1;
	int zj=check(mid,sh[x].db,z);
	if(zj==1)swap(sh[x].db,z);//如果当前节点在中间比上一个数低
	int zb=check(l,sh[x].db,z),yb=check(r,sh[x].db,z);
	if(zb==1)insert(sh[x].ls,l,mid,z);//左边比代表低 
	if(yb==1)insert(sh[x].rs,mid+1,r,z);//右边比代表低 
	return; 
}
void modify(int& x,int l,int r,int lt,int rt,int z)
{
	if(!x) x=++tot;
	if(lt<=l&&rt>=r)
	{
		insert(x,l,r,z);//抓出区间 
		return ;
	}
	int mid=(l+r)>>1;
	if(lt<=mid)modify(sh[x].ls,l,mid,lt,rt,z);
	if(rt>mid)modify(sh[x].rs,mid+1,r,lt,rt,z);
}
int main()
{
	freopen("defense.in","r",stdin);
	freopen("defense.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>m;
	for(int i=1;i<=m;i++)
	{
		int opt;cin>>opt;
		if(opt==1)
		{
			cnt++;
			cin>>f[cnt].x1>>f[cnt].y1>>f[cnt].x2>>f[cnt].y2;
			f[cnt].k=(f[cnt].y2-f[cnt].y1)*1.0/(f[cnt].x2-f[cnt].x1)*1.0;
			//cout<<f[i]
			modify(rt,1,inf,f[cnt].x1,f[cnt].x2,cnt); 
		}
		else
		{
			int a;cin>>a;
			cout<<query(rt,1,inf,a)<<'\n';	
		}
	}	
	return 0;	
} 

模板

如果你非常擅长 \(dp\) 的话,你一定做过斜率优化的题。
斜率优化是形如这样的\(dp[i]=Min/Max(a[i]∗b[j]+c[j])+d[i]\)的dp转移式子。
在除了主流的维护凸包的做法,我们换一种理解方式。如果上面那个式子理解为一个函数,显然是一个一次函数\(y=kx+b\)的式子,对于每个 \(j\),\(b[j]\)就是 \(k\),\(c[j]\)就是 \(b\),\(a[i]\) 就是 \(x\)。然后求极值,不就是一堆直线(函数但函数图像就是直线)求单点最值,无脑上李超线段树即可。复杂度为 \(O(n\log n)\)
复杂度不一定是最优的,但是非常无脑只用推出式子,不用任何性质,还非常好写。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+10,inf=1e18+10; 
int n,sum[N],h[N],dp[N],sh[N<<2],mx;
struct node
{
	int k,b;
}bi[N];
int cmp(int x,int y){return x>y;}
int calc(int k,int wz){return bi[k].k*wz+bi[k].b;} 
void modify(int x,int l,int r,int k)
{
	int mid=(l+r)>>1;
	int fmid=cmp(calc(sh[x],mid),calc(k,mid));//比较sh[x]代表的线段与k代表的线段在mid处的大小关系
	if(fmid==1)swap(sh[x],k);//保证中点的地方sh[x]比k低
	int fl=cmp(calc(sh[x],l),calc(k,l)),fr=cmp(calc(sh[x],r),calc(k,r));
	if(fl==1)modify(x<<1,l,mid,k);//如果与左区间有交点 
	if(fr==1)modify(x<<1|1,mid+1,r,k);//如果与右区间有交点 
	return ; 
}
node nmix(node x,node y)
{
    int f=cmp(x.k,y.k);
    if(f)return y;
    return x;
}
node query(int x,int l,int r,int k)
{
	if(r<k||l>k)return {inf,inf}; //不在范围 
	int mid=(l+r)>>1;
	int zhi=calc(sh[x],k);//计算在x的位置 
	if(l==r){return {zhi,sh[x]};}//在子节点 
	return nmix({zhi,sh[x]},nmix(query(x<<1,l,mid,k),query(x<<1|1,mid+1,r,k)));
}
signed main()
{
	cin>>n;
	for(int i=1;i<=n;i++)cin>>h[i],mx=max(h[i],mx);
	for(int i=1;i<=n;i++)
	{
		int x;cin>>x; 
		sum[i]=sum[i-1]+x;
	}
	bi[0]={0,inf};
	modify(1,0,mx,0); 
	bi[1].k=-2*h[1],bi[1].b=dp[1]+h[1]*h[1]-sum[1];//第一根柱子的情况 
	modify(1,0,mx,1); 
	for(int i=2;i<=n;i++)
	{
		int k=query(1,0,mx,h[i]).k; 
		dp[i]=k+sum[i-1]+h[i]*h[i];
		bi[i].k=-2*h[i],bi[i].b=dp[i]+h[i]*h[i]-sum[i];
		modify(1,0,mx,i);
	}
	cout<<dp[n]<<'\n'; 
	return 0; 
} 

拓展

其实还有一个广义李超线段树。
我们发现其实李超线段树并不依赖直线,只需要满足两个性质:

  1. 任意两个函数只有一个交点
  2. 可以快速求出函数某个位置的值
    所以在某些题中,如果满足这些性质也可以仿照李超线段树的写法来做。

详见这里

posted @ 2025-03-16 21:54  exCat  阅读(60)  评论(0)    收藏  举报