学习NOTE——10 李超线段树

李超真是个____。这都想得到。

引入(?)

我们要在平面直角(笛卡尔)坐标系中进行一些操作:

  1. 动态插入一条线段(或直线);
  2. 单点查询:给定一个横坐标\(x\),查询所有已插入线段在该点处的纵坐标最大值,并输
    出对应线段编号。

但是朴素的暴力在每次查询时都是\(O(n)\)的,肥肠的不优。
但是,李超线段树可以在\(O(\log{C})\)的时间里做到!(\(C\)为坐标范围大小)

前置芝士:线段树,一次函数

是什么

李超线段树(Li Chao Tree)是一种可以求函数定点最值的线段树,代码 一点都不 简短,思想 一点都不 简明,用途 是真的 广泛。
既然叫“线段树”,那么写法是差不多的,只是加了一些数学小知识而已(吗)。

怎么实现

到重点了!!

存线段

  1. 首先,要存每条线的信息吧。
    两种办法:
  • 存起点与终点的坐标 \((x1,y1),(x2,y2)\)
    struct Line{int x0,y0,x1,y1;}l[N];
    
  • 存斜率(比例系数)与截距(一般用于 dp 题)
    struct Line{
      int k,b;
      Line(){k=0,b=0;}//赋初值
      //主要目的就是建一个哨兵节点,防止访问到空的
    }l[N];
    
  1. 既然是线段树,那肯定要存线段树的节点啊。
    struct Tree{int l,r,bst;}tr[N<<2];
    //这里的bst是指当前节点代表区间的最优线段编号
    
对于最优线段的解释

image

这里对于区间 \([0,8]\) 的最优线段是绿色的,为什么是绿色的呢?因为李超线段树维护的是在当前区间中点处最大/小的。

为什么是中点 选择中点是为了保证树的平衡性和操作的正确性。

通过比较中点处的函数值,
可以决定新插入的线段是否应该“覆盖”当前节点存储的线段,或者递归到子区间。

插入

大概的流程:对于每条线段,我们都不断二分(像线段树一样),直到它完全覆盖了一些区间(且没有别的空白),再在这些区间内更新最优线段,最后查看被淘汰下来的线段在哪里更优,就递归哪一棵子树。

模拟一下:
考虑在一个\([1,20]\)的区间内进行操作,加入一条蓝线。
image

不断二分:
image

接着判断最优,既然没有别的线段,那就直接加。(剩下的因为我太懒就不画了)
image

加入一条绿线:
image

依旧二分。
如果绿线完全覆盖了每个区间,但蓝线没有的(比如\([1,5]\)),直接更新为绿色的。但如果两个都覆盖了的(比如\([6,10]\)),那我们就要去比较一下,别忘了是在中点处比较。
最后看淘汰掉的线有没有在左/右子树中更优(这可不能忘了)。(这里蓝色的是蓝线,绿色的是绿线,黄色的是被修改过的)(依旧不画完)
image


总结一下:

  • 若当前区间还没记录最优线段,那么直接记录并返回。
  • 若当前区间的最优线段新插入的线段完全覆盖,则把最优线段修改为新插入线段并返回。
  • 若当前区间的最优线段把新插入线段完全覆盖,则直接返回。
  • 若当前区间最优势线段与新插入线段有交,则先判断哪条线段在当前区间更优,并把不优的线段下
    传到交点所在子区间。(交点两边的部分分别被这两条线控制,既然我们已经让中点处更优的那条
    线段作为最优线段,因此不优的那条只可能在交点所在子区间超过最优
    线段)

查询

查询就很简单啦,就像普通线段树一样,走到底,然后找最优线段。因为李超线段树采用了标记永久化,每个节点存储的线段并不一定是全局在该点处最优的,则需要从根节点到叶子节点路径上所有节点的优势线段中取最优。
就像这样:比如说要查 \(x=3\) 的最优线段,直接走到底:
image

代码

就拿板子题和一道dp的举例吧。


首先是关于线段的信息与计较,存的话可以存\((x_0,y_0),(x_1,y_1)\),也可以存\(k,b\)(上面的存什么板块也讲了)。
既然我们知道了线段的信息,那要求一个在线段上的\(y\)该怎么求呢?有个公式 (但我不会推QAQ):\(y=\frac{(x-x_0)(y_0-y_1)}{x_0-x_1}\),注意精度问题。
然后是比较,我们可以直接比在当前这个\(x\),谁的\(y\)更优。

struct Line{int x0,y0,x1,y1;}l[N];
struct Tree{int l,r,bst;}tr[N<<2];
double _y(int p,int x){//计算x在线段p上的纵坐标
	if(l[p].x0==l[p].x1) return max(l[p].y0,l[p].y1);//x0==x1
	return 1.0*(x-l[p].x0)*(l[p].y0-l[p].y1)/(l[p].x0-l[p].x1)+l[p].y0;
}
int cmp(double a,double b){//比较哪y个更大,相同则序号小的优先
	double ep=1e-9;
	if(a-b>ep) return 1;
	if(b-a>ep) return -1;
	return 0;
}
int pmx(int a,int b,int x){
	int c=cmp(_y(a,x),_y(b,x));
	if(c==1||(c==0&&a<b)) return a;
	return b;
}

接下来就到了代码中的重点,修改,既然我们已经讲过了操作并模拟了,应该会好理解一点。
先判是否在线段的区间内,若完全覆盖了则与原本这里存比较的谁更优,否则递归左右子树,直到完全覆盖了一些区间。

void modify(int p,int l,int r,int op){
	if(tr[p].l>r||tr[p].r<l) return ;
	if(tr[p].l>=l&&tr[p].r<=r){//完全覆盖
		int mid=(tr[p].l+tr[p].r)/2;
		//若新线段在中点更优(判断是否更优)
		if(pmx(op,tr[p].bst,mid)==op) swap(op,tr[p].bst);
		//若淘汰的在左子树更优,递归左子树
		if(pmx(op,tr[p].bst,tr[p].l)==op) modify(p<<1,l,r,op);
		//若淘汰的在右子树更优,递归右子树
		if(pmx(op,tr[p].bst,tr[p].r)==op) modify(p<<1|1,l,r,op);
		return ;
	}
	//部分覆盖,递归左右子区间(二分)
	modify(p<<1,l,r,op);
	modify(p<<1|1,l,r,op);
}

查询的就和普通线段树没什么两样了。
若当前的节点是叶子,直接返回最优线段并结束,否则就一直走,到叶子上为止。别忘了要与之前走过的所有线比较谁更优。

int query(int p,int x){
	if(tr[p].l==tr[p].r) return tr[p].bst;
	int mid=(tr[p].l+tr[p].r)/2;
	if(x<=mid) return pmx(tr[p].bst,query(p<<1,x),x);
	else return pmx(tr[p].bst,query(p<<1|1,x),x);
}

最后是板子题的代码

$\color{Cyan}Code$
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+10;
struct Line{int x0,y0,x1,y1;}l[N];
struct Segment_T{int l,r,bst;}tr[N<<2];
double _y(int p,int x){//计算x在线段p上的纵坐标
	if(l[p].x0==l[p].x1) return max(l[p].y0,l[p].y1);//x0==x1
	return 1.0*(x-l[p].x0)*(l[p].y0-l[p].y1)/(l[p].x0-l[p].x1)+l[p].y0;
}
int cmp(double a,double b){//比较哪y个更大,相同则序号小的优先
	double ep=1e-9;
	if(a-b>ep) return 1;
	if(b-a>ep) return -1;
	return 0;
}
int pmx(int a,int b,int x){
	int c=cmp(_y(a,x),_y(b,x));
	if(c==1||(c==0&&a<b)) return a;
	return b;
}
void build(int p,int l,int r){
	tr[p].l=l,tr[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 modify(int p,int l,int r,int op){
	if(tr[p].l>r||tr[p].r<l) return ;
	if(tr[p].l>=l&&tr[p].r<=r){//完全覆盖
		int mid=(tr[p].l+tr[p].r)/2;
		//若新线段在中点更优(判断是否更优)
		if(pmx(op,tr[p].bst,mid)==op) swap(op,tr[p].bst);
		//若淘汰的在左子树更优,递归左子树
		if(pmx(op,tr[p].bst,tr[p].l)==op) modify(p<<1,l,r,op);
		//若淘汰的在右子树更优,递归右子树
		if(pmx(op,tr[p].bst,tr[p].r)==op) modify(p<<1|1,l,r,op);
		return ;
	}
	//部分覆盖,递归左右子区间
	modify(p<<1,l,r,op);
	modify(p<<1|1,l,r,op);
}
int query(int p,int x){
	if(tr[p].l==tr[p].r) return tr[p].bst;
	int mid=(tr[p].l+tr[p].r)/2;
	if(x<=mid) return pmx(tr[p].bst,query(p<<1,x),x);
	else return pmx(tr[p].bst,query(p<<1|1,x),x);
}
int n,k,lst,cnt;
int calc1(int x){return (x+lst-1)%39989+1;}
int calc2(int x){return (x+lst-1)%1000000009+1;}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n;
	build(1,1,40000);
	for(int i=1,op,x0,x1,y0,y1,k;i<=n;i++){
		cin>>op;
		if(op==1){
			cin>>x0>>y0>>x1>>y1;
			x0=calc1(x0),x1=calc1(x1);
			y0=calc2(y0),y1=calc2(y1);
			if(x0>x1) swap(x0,x1),swap(y0,y1);
			l[++cnt]={x0,y0,x1,y1};
			modify(1,x0,x1,cnt);
		}
		else{
			cin>>k;
			k=calc1(k);
			lst=query(1,k);
			cout<<lst<<"\n";
		}
	}
	return 0;
}

别忘了在存之后要调用修改函数更新。


然后是dp题。
公式很好推,就是这个(\(dp_i\)表示已经修了前\(i\)个桥的最小花费,\(p_i\)为\(w\)的前缀和,\(h\)就是题面上的):

\[dp_i=dp_j+(h_i-h_j)^2+p_{i-1}-p_j \]

展开:

\[dp_i=h_i^2+p_{i-1}-2\times h_i\times h_j+h_j^2-p_j+dp_j \]

我们像斜率优化一样把\(i\)放在前\(j\)放在后:

\[dp_i-h_i^2-p_{i-1}=-2\times h_i\times h_j+h_j^2-p_j+dp_j \]

把等号前的当成\(y\),\(-2\times h_i\times h_j\)的 \(h_j\)当成\(k\),\(h_i\)当成\(x\),因为在当前这个\(x\)时,我们能查到的最优的线就是我们要的\(j\)。
最后就成了这样:

\[dp_i=y+h_i^2+p_{i-1}=-2·k·x+h_j^2+p_j+dp_j+h_i^2+p_{i-1} \]

$\color{Cyan}Code$
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10,M=1e6+10;
struct Line{//这里记录的是k,b
	int k,b;
	Line(){k=0,b=LLONG_MAX;}
}l[N];
struct Segment_T{int l,r,bst;}tr[M<<2];
double _y(int p,int x){return -2*l[p].k*x+l[p].b;}
int cmp(double a,double b){
	double ep=1e-9;
	if(a-b>ep) return -1;
	if(b-a>ep) return 1;
	return 0;
}
int pmn(int a,int b,int x){
	int c=cmp(_y(a,x),_y(b,x));
	if(c==1||(c==0&&a<b)) return a;
	return b;
}
void build(int p,int l,int r){
	tr[p].l=l,tr[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 modify(int p,int l,int r,int op){
	if(tr[p].l>r||tr[p].r<l) return ;
	if(tr[p].l>=l&&tr[p].r<=r){
		int mid=(tr[p].l+tr[p].r)/2;
		if(pmn(op,tr[p].bst,mid)==op) swap(op,tr[p].bst);
		if(pmn(op,tr[p].bst,tr[p].l)==op) modify(p<<1,l,r,op);
		if(pmn(op,tr[p].bst,tr[p].r)==op) modify(p<<1|1,l,r,op);
		return ;
	}
	modify(p<<1,l,r,op);
	modify(p<<1|1,l,r,op);
}
int query(int p,int x){
	if(tr[p].l==tr[p].r) return tr[p].bst;
	int mid=(tr[p].l+tr[p].r)/2;
	if(x<=mid) return pmn(tr[p].bst,query(p<<1,x),x);
	else return pmn(tr[p].bst,query(p<<1|1,x),x);
}
int n,h[N],w[N],dp[N];
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>h[i];
	for(int i=1;i<=n;i++) cin>>w[i],w[i]+=w[i-1];
	int mx=*max_element(h+1,h+n+1);
	build(1,0,mx);
	l[1].k=h[1],l[1].b=h[1]*h[1]-w[1];
	modify(1,0,mx,1);
	for(int i=2;i<=n;i++){
		int j=query(1,h[i]);
		dp[i]=-2*l[j].k*h[i]+l[j].b+h[i]*h[i]+w[i-1];
		l[i].k=h[i];
		l[i].b=dp[i]+h[i]*h[i]-w[i];
		modify(1,0,mx,i);
	}
	cout<<dp[n];
	return 0;
}

为什么这道题要用李超线段树,不用斜率优化呢?
因为这里的\(b\)不具有单调性,哪怕\(k\)很优,但也无法确定是否是最优的,二分/单调队列就不行了。
顺便安利一下我的斜率优化笔记。

posted @ 2026-10-16 10:20  _T_M_T  阅读(59)  评论(0)    收藏  举报