李超线段树

你说什么?我一上午就敲了个板子?

李超线段树

定义

李超线段树是一种用于维护多条一次函数的线段树。你可以使用它在\(O(logn)\)的复杂度内插入一条新的直线,或是查询所有直线\(y=k_ix+b_i\)中,当\(x=x_0\)时,\(y\) 的最值。

例题

洛谷 P4097 【模板】李超线段树 / [HEOI2013] Segment

image

理论

插入

线段树上每个节点维护\(x=\frac {l+r} {2}\)处的最高点的 \(y\)和线段编号。
对于区间\(l~r\),插入一条新线段。

  1. 该线段与区间\(l~r\)没有交集
    因此对区间内维护的最优线段没有影响;

image

  1. 该线段与区间\(l~r\)有交集但区间不是该线段定义域的子集
    此时考虑递归到该区间的左右子区间作进一步细化处理;

image

  1. 区间\(l~r\)是该线段定义域的子集
    此时分三种情况分类讨论原区间\(l~r\)中的最优线段和新加线段的位置关系:
    (红色为新线段,黑色为老线段即原最优线段)
  • 新线段在老线段的上方时:直接更新即可。

image

  • 新线段在老线段的下方时:丢弃。
    image

  • 新线段和老线段交叉时:
    设点\(x\)是线段树上的一个节点,\(mid\)\(x\)所代表的区间的中点。
    原来,使\(y\)取到最大值的直线编号是\(t[x]\)(即黑色线段),现在新增的直线编号是\(now\) (即红色线段)。
    我们比较在区间中点 \(mid\) 两条线段对应的纵坐标谁大,将大的那条作为新的最优线段(即老线段)。
    但由于这两条线段相互之间都不能被对方完全覆盖,所以考虑将没成为最优线段的那条线段往该区间的左右子区间下放,作递归处理(只需要往有比最优线段更优的一端递归即可)。

如图:
image

红色的线段为新线段,黑色的线段为老线段(即原最优线段),比较两个线段上当\(x=mid\)\(y\)的大小,发现新线段的\(y\)更大,交换新线段和老线段。
如图
image
判断可知新线段(即红色线段)的左区间端点比老线段要大,递归更新左区间。判断出右区间新线段不优,即不能对答案做出贡献,丢弃。

每一次都只会修改左右区间中的\(1\)个,时间复杂度为\(O(log^2n)\)。我们把这种对 区间修改、单点查询 起效的方法称作“标记永久化”。

查询

单点查询
需要注意的是由于 标记永久化 的存在,路径上遇到的任意一个答案都可能是最终的答案,而叶子节点处可能根本没有记录答案。
所以需要将路径上遇到的所有答案取最值作为最终结果。
如上图中右区间的任意一个点的答案都在 \(x\) 节点处被记录,而没有继续往下传递。所以需要在 \(x\) 处求出答案并和其它可能答案取 \(max\)

实现

代码

#include<bits/stdc++.h>
using namespace std;
#define ls (ro<<1)
#define rs (ro<<1|1)
const double eps=1e-10;//调精度
const int mod=39989;
const int _mod=1e9;
int n,op,k,xx,xy,yx,yy;
int ans,lastans,lcnt;
struct jade
{
    double k,b;//直线参数 
}line[100010];//线段 
struct seek
{
    int l,r,id;//区间左右端点//编号 
}t[400010];
//李超线段树 
void build(int ro,int l,int r)
{
    t[ro].l=l;
    t[ro].r=r;
    if(l==r)
    {
        return ;
    }
    int mid=(l+r)>>1;
    build(ls,l,mid);
    build(rs,mid+1,r);
}
double gety(int i,int x)//计算纵坐标 
{
    return line[i].k*x+line[i].b;
}
bool cmp(int i,int j,int x)//比较编号为 i 和 j 的线段在 X 处的纵坐标 
{
    if(gety(i,x)-gety(j,x)>eps)
    {
        return 1;
    }
    if(gety(j,x)-gety(i,x)>eps)
    {
        return 0;
    }
    return i<j;
}
void insert(int ro,int l,int r,int id)//插入线段,编号为 id 
{
    if(t[ro].r<l||t[ro].l>r)//线段与该区间不交 
    {
        return ;
    }
    if(l<=t[ro].l&&r>=t[ro].r)//线段完全覆盖该区间,区间是该线段定义域的子集
    {
        if(cmp(id,t[ro].id,t[ro].l)&&cmp(id,t[ro].id,t[ro].r))//该线段比当前区间最优线段完全更优 
        {
            t[ro].id=id;
            return ;
        }
        if(cmp(id,t[ro].id,t[ro].l)==0&&cmp(id,t[ro].id,t[ro].r)==0)//该线段比当前区间最优线段完全更劣
        {
            return ;
        }
        //两线段有交点,都不能完全覆盖彼此
        int mid=(t[ro].l+t[ro].r)>>1;
        if(cmp(id,t[ro].id,mid))
        {
            swap(id,t[ro].id);
        }
        //处理左右子区间 
        if(cmp(id,t[ro].id,t[ro].l))
        {
            insert(ls,l,r,id);
        }
        if(cmp(id,t[ro].id,t[ro].r))
        {
            insert(rs,l,r,id);
        }
    }
    else//线段覆盖部分该区间 
    {
        //处理左右子区间 
        insert(ls,l,r,id);
        insert(rs,l,r,id);
    }
}
void query(int ro,int x)
{
    if(t[ro].r<x||t[ro].l>x)
    {
        return ;
    }
    if(cmp(t[ro].id,ans,x))//比较路径上的最优线段,记录编号 
    {
        ans=t[ro].id;
    }
    if(t[ro].l==t[ro].r)
    {
        return ;
    }
    //在树上往下查询最优线段
    query(ls,x);
    query(rs,x);
}
int main()
{
    cin>>n;
    build(1,1,mod);
    for(int i=1;i<=n;i++)
    {
        cin>>op;
        if(op==0)
        {
            cin>>k;
            k=(k+lastans-1)%mod+1;
            ans=0;
            query(1,k);
            cout<<ans<<endl;
            lastans=ans;
        }
        else
        {
            cin>>xx>>xy>>yx>>yy;
            xx=(xx+lastans-1)%mod+1;
            yx=(yx+lastans-1)%mod+1;
            xy=(xy+lastans-1)%_mod+1;
            yy=(yy+lastans-1)%_mod+1;
            if(yx<xx)
            {
                swap(xx,yx);
                swap(xy,yy);
            }
            lcnt++;
            if(yx!=xx)
            {
                line[lcnt].k=1.0*(yy-xy)/(yx-xx);
                line[lcnt].b=1.0*xy-line[lcnt].k*xx;
            }
            else//特殊处理竖直线段
            {
                line[lcnt].k=0;
                line[lcnt].b=max(xy,yy);
            }
            insert(1,xx,yx,lcnt);
        }
    }
    return 0;
}

来自机房大蛇们的另一种做法

即把原insert函数拆为insert和add,分开处理线段与区间的交集情况。
add处理线段与区间的关系,若完全覆盖则调用insert加入线段,此时因区间全覆盖所以只传参\(id\)\(ro\)
反之则add函数向左右儿子递归。
add函数的时间复杂度为\(O(\log n)\),insert函数的时间复杂度也为\(O(\log n)\),合并起来为\(O(\log^2n)\),时间复杂度相同。

#include<bits/stdc++.h>
using namespace std;
#define ls (ro<<1)
#define rs (ro<<1|1)
const double eps=1e-10;//调精度
const int mod=39989;
const int _mod=1e9;
int n,op,k,xx,xy,yx,yy;
int ans,lastans,lcnt;
struct jade
{
    double k,b;//直线参数 
}line[100010];//线段 
struct seek
{
    int l,r,id;//区间左右端点//编号 
}t[400010];
//李超线段树 
void build(int ro,int l,int r)
{
    t[ro].l=l;
    t[ro].r=r;
    if(l==r)
    {
        return ;
    }
    int mid=(l+r)>>1;
    build(ls,l,mid);
    build(rs,mid+1,r);
}
double gety(int i,int x)//计算纵坐标 
{
    return line[i].k*x+line[i].b;
}
bool cmp(int i,int j,int x)//比较编号为 i 和 j 的线段在 X 处的纵坐标 
{
    if(gety(i,x)-gety(j,x)>eps)
    {
        return 1;
    }
    if(gety(j,x)-gety(i,x)>eps)
    {
        return 0;
    }
    return i<j;
}
void insert(int ro,int id)//插入直线,编号为 id 
{
    if(cmp(id,t[ro].id,t[ro].l)&&cmp(id,t[ro].id,t[ro].r))//该直线比当前区间最优线段完全更优 
    {
        t[ro].id=id;
        return ;
    }
    if(cmp(id,t[ro].id,t[ro].l)==0&&cmp(id,t[ro].id,t[ro].r)==0)//该直线比当前区间最优线段完全更劣
    {
        return ;
    }
    //两线段有交点,都不能完全覆盖彼此
    int mid=(t[ro].l+t[ro].r)>>1;
    if(cmp(id,t[ro].id,mid))
    {
        swap(id,t[ro].id);
    }
    //处理左右子区间 
    if(cmp(id,t[ro].id,t[ro].l))
    {
        insert(ls,id);
    }
    if(cmp(id,t[ro].id,t[ro].r))
    {
        insert(rs,id);
    }
    return ;
}
void add(int ro,int l,int r,int id)
{
    if(t[ro].r<l||t[ro].l>r)//线段与该区间不交
    {
    	return ;	 
	}	
	if(l<=t[ro].l&&r>=t[ro].r)//线段完全覆盖该区间,区间是该线段定义域的子集
	{
		insert(ro,id);
		return ;
	}
	//线段覆盖部分该区间 
    //处理左右子区间 
    add(ls,l,r,id);
    add(rs,l,r,id);
} 
void query(int ro,int x)
{
    if(t[ro].r<x||t[ro].l>x)
    {
        return ;
    }
    if(cmp(t[ro].id,ans,x))//比较路径上的最优线段,记录编号 
    {
        ans=t[ro].id;
    }
    if(t[ro].l==t[ro].r)
    {
        return ;
    }
    //在树上往下查询最优线段
    query(ls,x);
    query(rs,x);
}
int main()
{
    cin>>n;
    build(1,1,mod);
    for(int i=1;i<=n;i++)
    {
        cin>>op;
        if(op==0)
        {
            cin>>k;
            k=(k+lastans-1)%mod+1;
            ans=0;
            query(1,k);
            cout<<ans<<endl;
            lastans=ans;
        }
        else
        {
            cin>>xx>>xy>>yx>>yy;
            xx=(xx+lastans-1)%mod+1;
            yx=(yx+lastans-1)%mod+1;
            xy=(xy+lastans-1)%_mod+1;
            yy=(yy+lastans-1)%_mod+1;
            if(yx<xx)
            {
                swap(xx,yx);
                swap(xy,yy);
            }
            lcnt++;
            if(yx!=xx)
            {
                line[lcnt].k=1.0*(yy-xy)/(yx-xx);
                line[lcnt].b=1.0*xy-line[lcnt].k*xx;
            }
            else//特殊处理竖直线段
            {
                line[lcnt].k=0;
                line[lcnt].b=max(xy,yy);
            }
            add(1,xx,yx,lcnt);
        }
    }
    return 0;
}

我们发现,如果线段树维护的是直线,则使用\(O(\log n)\)做法即可完成修改操作,因为不用判断区间,多用于DP优化方面。

例题2.0(NOIP2025模拟1 C. 舰队的远征)

更新于2025/11/4
今天是第一场noip模拟赛,炸炸炸,改改改。
神秘的T3可以用李超线段树写,但我又懒得新开一篇博客,所以就归类到李超线段树的例题里惹(QAQ是因为根本没空写,现在还在施工的有"CSP-S 2025 游记","2-SAT学习笔记"…………)
题面link

赛时

花4min写了一个弗洛伊德,创了40pts,太傻比了所以就不展开讲讲了

正解

dij预处理+李超线段树
hiahiahiahiahiahiahiahiahiahihahaihaihai(这题卡SPFA!!😦 )

发现如果没有隐藏能源,这题是平凡的(我直接一个单源最短路),但考虑其消耗燃料的表达式,设临时通道的起点为\(x\),终点是\(y\),其舰队从\(s\)出发,抵达\(t\),则有:

\[cost=dis_s[x]+dis_t[y]+(x-y)^2 \]

\[x\in[1,n],y\in[1,n] \]

tips:
\(dis_s[x]\)表示从\(s\)出发到\(x\)点的最短路。
\(dis_t[y]\)表示从\(t\)出发到\(y\)点的最短路。
这两个东西可以用两遍\(dij\)跑出来,但区别是一个在正向建边的图跑,另一个是在反向建边的图跑。

啊这东西即\(x\)\(y\)的不太好优化,考虑将它拆开

\[cost=dis_s[x]+x^2+dis_t[y]+y^2-2\times xy \]

考虑枚举\(x\),发现\(dis_s[y]+y^2-2\times xy\)形如一次函数\(y=kx+b\)的形式,然后套用李超线段树求解给定\(x\)关于\(y\)的最小贡献(即使\(dis_t[y]+y^2-2\times xy\)最小),然后加上枚举\(x\)得到的\(dis_s[x]+x^2\)值,最后在\(n\)个答案中取最小值即可。

代码实现

#include<bits/stdc++.h>
using namespace std;
#define ls (ro<<1)
#define rs (ro<<1|1)
struct jade
{
	long long l,r,id;
}tr[800010];
long long n,m,s,t;
long long h[200010],to[400010],nxt[400010],v[400010],tot;
long long dis[200010];
bool vis[200010];
long long _h[200010],_to[400010],_nxt[400010],_v[400010],_tot;
long long _dis[200010];
bool _vis[200010];
long long k[200010],b[200010];
long long cnt;
void add(long long x,long long y,long long val)
{
	tot++;
	to[tot]=y;
    nxt[tot]=h[x];
    v[tot]=val;
	h[x]=tot;	
}
void _add(long long x,long long y,long long val)
{
	_tot++;
	_to[_tot]=y;
	_nxt[_tot]=_h[x];
	_v[_tot]=val;
	_h[x]=_tot;
}
void dij()
{
	memset(dis,0x3f,sizeof(dis));
	priority_queue<pair<long long,long long>,vector<pair<long long,long long>>,greater<>>q;
	q.push(make_pair(0,s));
	dis[s]=0;
	while(!q.empty())
	{
	    long long x=q.top().second;
		q.pop();	
		if(vis[x])
		{
			continue;
		}
		vis[x]=1;
		for(long long i=h[x];i;i=nxt[i])
		{
			long long y=to[i];
			if(dis[y]>dis[x]+v[i])
			{
			    dis[y]=dis[x]+v[i];
			    q.push(make_pair(dis[y],y));
			} 
		}
	} 
}
void _dij()
{
	memset(_dis,0x3f,sizeof(_dis));
	priority_queue<pair<long long,long long>,vector<pair<long long,long long>>,greater<>>q;
	q.push(make_pair(0,t));
	_dis[t]=0;
	while(!q.empty())
	{
	    long long x=q.top().second;
		q.pop();	
		if(_vis[x])
		{
			continue;
		}
		_vis[x]=1;
		for(long long i=_h[x];i;i=_nxt[i])
		{
			long long y=_to[i];
			if(_dis[y]>_dis[x]+_v[i])
			{
			    _dis[y]=_dis[x]+_v[i];
			    q.push(make_pair(_dis[y],y));
			} 
		}
	} 
}
void build(long long ro,long long l,long long r)
{
	tr[ro].l=l;
	tr[ro].r=r;
	if(l==r)
	{
		return ;
	}
	long long mid=(l+r)>>1;
	build(ls,l,mid);
	build(rs,mid+1,r);	
}
long long js(long long id,long long x)
{
    return k[id]*x+b[id];
}
bool cmp(long long i,long long j,long long x)
{
	if(js(i,x)>js(j,x))
	{
		return 0;
	}
	return 1;
}
void insert(long long ro,long long id)
{
	if(cmp(id,tr[ro].id,tr[ro].l)&&cmp(id,tr[ro].id,tr[ro].r))
	{
		tr[ro].id=id;
		return ;
	}
	if(cmp(id,tr[ro].id,tr[ro].l)==0&&cmp(id,tr[ro].id,tr[ro].r)==0)
	{
		return ;
	}
	long long mid=(tr[ro].l+tr[ro].r)>>1;
	if(cmp(id,tr[ro].id,mid))
	{
		swap(id,tr[ro].id); 
	}
	if(cmp(id,tr[ro].id,tr[ro].l))
	{
		insert(ls,id);
	}
	if(cmp(id,tr[ro].id,tr[ro].r))
	{
	    insert(rs,id);
	}	
}
void query(long long ro,long long x)
{
	if(tr[ro].r<x||tr[ro].l>x)
	{
		return ;
	}
	if(cmp(tr[ro].id,cnt,x)) 
	{
		cnt=tr[ro].id;
	}
	if(tr[ro].l==tr[ro].r)
	{
	    return ;	
	}
	query(ls,x);
	query(rs,x);
}
int main()
{
	freopen("far.in","r",stdin);
	freopen("far.out","w",stdout); 
    cin>>n>>m>>s>>t;
	for(long long i=1;i<=m;i++)
	{
	    long long x,y,val;
		cin>>x>>y>>val;
		add(x,y,val);
		_add(y,x,val);	
	} 
	dij();
	_dij();
	k[0]=0;
	b[0]=1e18;
	for(long long i=1;i<=n;i++)
	{
		k[i]=-2*i;
		b[i]=_dis[i]+i*i;
	}
	build(1,1,n);
	for(long long i=1;i<=n;i++)
	{
	    insert(1,i);	
	} 
	long long ans=1e18;
	for(long long i=1;i<=n;i++)
	{
		cnt=0;
		query(1,i);
		long long res=dis[i]+i*i+b[cnt]+i*k[cnt]; 
		ans=min(ans,res);
	}
	cout<<ans;
    return 0;
}

不开longlong见祖宗

李超线段树合并优化DP

李超/线段树合并/优化DP
更新于2025/10/25
模莫默模拟赛的T4正解是李超/线段树合并/优化DP,其实也属于动态开点部分。
发现我的李超线段树连板子都忘得差不多了,这题还是Pursuing_OIer学长讲过的原题(对不起)
于是回炉重造一下。

定义

你问我啥是李超线段树合并优化DP,其实就是李超/线段树合并/优化DP,额就是用动态开点的李超线段树做李超线段树合并维护斜率来优化DP
你可以把它看成李超线段树+动态开点+线段树合并+DP
还是不知道?那就看一下例题:

例题

(模莫默模拟赛原题哈哈哈因为数据太水放过了许多\(n^2\)
洛谷 CF932F Escape Through Leaf

image

考虑\(DP\),设\(dp[i]\)为从x出发到叶子节点的最小代价(即答案),发现一个十分显然(但不会维护的)动态转移方程:

\[dp_x=min(dp_y+a_x×b_y)y∈subtree(x) \]

直接转移是\(O(n^2)\)的,肯定过不了(如果数据不那么水的话)

\(dp_y,b_y\)分别看作 \(b,k\)发现其很像一次函数的形式,此题又要求该方程的最值。(应该算斜率优化DP?优化斜率DP)

考虑使用李超线段树维护。

但发现每一次转移都需要一颗李超线段树,时间复杂度是\(O(n\log n^2)\)的,太坏惹!

然后就是(李超)线段树合并了。

时间复杂度\(O(n\log n)\)

细节请看代码实现:

代码

#include<bits/stdc++.h>
using namespace std;
struct jade
{
    long long k,b;//斜率,截率 
    int id;//标号 
}line[400010];//存线段 
int n,a[100010],b[100010];//输入 
long long dp[100010];//dp状态 
int h[100010],to[200010],nxt[200010],tot;//链式前向星 
int rt[2000010],ls[2000010],rs[2000010],js;//动态开点李超线段树 
void add(int x,int y)//存图 
{
    tot++;
    to[tot]=y;
    nxt[tot]=h[x];
    h[x]=tot;
}
long long get_y(jade i,int x)//找纵坐标 
{
    return i.k*x+i.b;
}
long long lsi_x(jade x,jade y)//找交点 
{
    return (x.b-y.b)/(y.k-x.k);
}
void insert(int &ro,int l,int r,jade ln)//动态开点插入一条线段 
{
    if(!ro)
    {
        js++;
        ro=js;
    }
    int mid=(l+r)>>1;
    if(get_y(line[ro],mid)>get_y(ln,mid)||!line[ro].id)//更换新老线段 
    {
        swap(line[ro],ln);
    }
    if(l==r||line[ro].k==ln.k||!ln.id)
    {
        return ;
    }
    long long pos=lsi_x(line[ro],ln);//找交点 
    if(pos<l||pos>r)
    {
        return ;
    }
    if(ln.k>line[ro].k)
    {
        insert(ls[ro],l,mid,ln);
    }
    else
    {
        insert(rs[ro],mid+1,r,ln);
    }
}
int merge(int x,int y,int l,int r)//李超线段树合并 
{
    if(!x||!y)
    {
        return x+y;
    }
    insert(x,l,r,line[y]);
    int mid=(l+r)>>1;
    ls[x]=merge(ls[x],ls[y],l,mid);//左儿子 
    rs[x]=merge(rs[x],rs[y],mid+1,r);//右儿子 
    return x;
}
jade find(int ro,int l,int r,int x)//区间查找 
{
    if(l==r)
    {
        return line[ro];
    }
    int mid=(l+r)>>1;
    jade res;
    if(mid>=x)
    {
        res=find(ls[ro],l,mid,x);
    }
    else
    {
        res=find(rs[ro],mid+1,r,x);
    }
    if(!res.id||get_y(line[ro],x)<get_y(res,x))
    {
        return line[ro];
    }
    return res;
}
void dfs(int x,int fa)//树形DP 
{
    for(int i=h[x];i;i=nxt[i])
    {
        int y=to[i];
        if(y==fa)
        {
            continue;
        }
        dfs(y,x);
        rt[x]=merge(rt[x],rt[y],-100000,100000);//值域 
    }
    int minn=find(rt[x],-100000,100000,a[x]).id;
    dp[x]=1ll*a[x]*b[minn]+dp[minn];//dp方程 
    insert(rt[x],-100000,100000,{b[x],dp[x],x});
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    for(int i=1;i<=n;i++)
    {
        cin>>b[i];
    }
    for(int i=1;i<n;i++)
    {
        int x,y;
        cin>>x>>y;
        add(x,y);
        add(y,x);
    }
    dfs(1,0);
    for(int i=1;i<=n;i++)
    {
        cout<<dp[i]<<" ";
    }
    return 0;
}

动态开点李超线段树

正在施工中...

广义李超线段树

正在施工中...

未完待续...

posted @ 2025-08-25 11:56  BIxuan—玉寻  阅读(93)  评论(27)    收藏  举报