李超树学习笔记

引言

哇,好怀念呀~两周前才写了斜率优化的笔记,现在就可以爆切李超树了(笑)上次斜率优化的笔记总感觉没写好,我将磕头道歉并会努力将这次的笔记写得更加简单易懂!

虽说是引言吧,但是也要写一点有用的东西的!(确信)是不是斜率优化有一个小小的缺点,就是当 \(k\) 和 \(b\) 的单调性都不确定的时候时单调队列优化是失效的。

那这个李超线段树就可以解决这个问题。

学完李超树之后,就可以直接无视一切限制无脑秒掉所以形如 \(dpi=dpj+a_i*b_j+c_j+d_i\) 的转移了。

定义

这个李超树吧,听着挺邪乎,那到具体是什么嘞?我们朴素的线段树就是加入一个点然后做操作(比如求和求max啊上面的)那如果我们加入一个线段/直线呢?直观一点,就是动态加一个线段然后我要知道所有地方的最佳决策点。请见下图:

如果只有黑色一条线段的时候,虚线最厉害(也就是最靠上的线段)的决策点就是黑线。然后我们加一个绿色的线最厉害的决策就是绿线。最后我们加一个红色的线发现不影响虚线的决策点虚线上最优的还是绿色的线。

叠甲: 我图最后一排的叶子没有画完,因为画不下了。跪求原谅 \bx

我们大概就是要 \(O(nlog^2n)\) 求一个这样的东西。

加入

那应该咋整呢,考虑线段树分而治之的思想,我们不断缩小区间试试?

定义我们要合并的状态 \({l,r,best}\) 即为 \(l,r\) 的最佳选择(注意一定要是完全覆盖,比如一个线段的范围是\(1,3\)即使它很厉害但是它也无法对\(2,5\)这个区间做任何改变)

那具体加入线段的操作请跟我一起模拟一下~假设我们有一个区间 \(1,20\) 然后考虑加一个线段:

然后我们不断二分,一直到覆盖完全后:

因为没有其他线段的加入所以它的线段树就长这样:(有一些我没写,因为太懒惰了,会意即可)

那我们再加入一条线段,看看应该怎么操作:

继续从中间开始二分区间,如果绿色线段完全覆盖但是黑色线段不完全覆盖的(比如 \(1,5\))我们不去比较大小直接更新。但是如果他俩刚好都完全覆盖了某个区间(比如 \(6,10\)),那就要比较之后再去更新答案。下面是更新之后的线段树:(蓝色的地方就是线段树被更改后的值)

是不是很简单易懂~

总结一下(对于每次加一个线段时的更改):

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

查询

加入线段的理解了,怎么查询一个点 \(x\) 的最优线段嘞?

其实思想也差不多,请跟着我一起模拟吧~ 比如我们要查询 \(x=11\) 意义下的 \(best\) 我们就只需要走下面这一条粉色的路就可以了:

注意注意!我们一定要把整条路走完,然后取最优的 。不然又可能出现一条线段在这一整个不是一直是最优的,但是因为这个区间有线段覆盖所一定要有一个最优的线段作为代表选了一条线段作为代表。结果我们直接把答案给它覆盖了,就会错误地求出最优解!(详情请见代码实现的1*部分)

实现

宏观的我们知道了李超树的实现了。那代码是怎么写的,相信大家还是心里有点数了。如果具体不知道的请跟我一起梳理一下叭那么下面就是代码讲解,为了更加明确我们要求什么,建议先阅读一下[题目](<https://www.luogu.com.cn/problem/P4097>),等会我就是讲解的这道题

操作之前

  • 开一个存线段的数组,如果是直线的话建议存 \(k\) 和 \(b\) 会简单一点。但是如果是线段的话,建议使用 \((x0,y0)\) 和 \((x1,y1)\) ,就是找 \(y\) 时候会麻烦一些。(因为这道题是加入的线段,所以选用第二种方法)至于求 \(y\) 嘛,用一个公式:\(y=y_0+\frac{x-x_0y_0-y_1}{x_0-x_1}\)。(其实还挺简单的,自己推一下叭,我就不讲了)当然还需要一个比较,比较哪个更靠上。(一定要注意精度问题呀!)合并一下就变成了:

    cpp 复制代码
    namespace line {
    	struct lines {int x0,y0,x1,y1 ;} lin[N];
    	double fy(int id,int x) { // 求第 id 条线段在横坐标 x 处的纵坐标
    		if(lin[id].x0==lin[id].x1) return max(lin[id].y0,lin[id].y1) ;
    		return 1.0*(x-lin[id].x0)*(lin[id].y0-lin[id].y1)/(lin[id].x0-lin[id].x1)+lin[id].y0 ;
    	}
    	int cmp(double a,double b) {
    		if(a-b>eps) return 1 ;
    		if(b-a>eps) return -1 ;
    		return 0 ;
    	}
    	// 在横坐标 x 处,比较线段 a 和线段 b 谁更优
    	int supermx(int a,int b,int x) {
    		int opt=cmp(fy(a,x),fy(b,x)) ;
    		return (opt==1||(opt==0&&a<b))?a:b ;
    	}
    } 
  • 一些线段树的经典操作就没必要多说啦~ 就是需要维护的就是区间 \(l,r\) 和最佳答案 \(best\) 。然后建树需要注意的就是把没有最佳线段的初始化为 \(1\) 就可以。

现在终于可以开始我们的重头戏了~

  • 嗯......我们先实现一下讲的最多的加入线段吧~加入线段根据朴素线段树的经验,我们应该要整一个函数。然后不断二分区间,如果当前区间与要加入线段的区间完全不相交就可以直接结束。如果当前遍历到的区间被完全覆盖,就更新 \(best\)。都不成立的话,我们就缩小范围去找它的左儿子和右儿子去尝试更新答案。记得一定要遍历后面的区间,因为有可能当前区间太大了,在后面的区间不是最大值。从而出错的情况!

    cpp 复制代码
    void modify(int p,int l,int r,int id) {
    	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)>>1 ;
    		// 维护best
    		if(supermx(id,tr[p].best,mid)==id) swap(id,tr[p].best) ;
    		//如果新线段在左端点处更优,则递归插入左子树
    		if(supermx(id,tr[p].best,tr[p].l)==id) modify(ls,l,r,id) ;
    		//如果新线段在右端点处更优,则递归插入右子树
    		if(supermx(id,tr[p].best,tr[p].r)==id) modify(rs,l,r,id) ;
    		return ;
    	}
    	modify(ls,l,r,id) ;
    	modify(rs,l,r,id) ;
    }
  • 根据同样的思想,我们试着再写一个查询函数。如果当前是叶子,就直接返回叶子上存的 \(best\) 并结束就可以。否则的话,我们就一路往叶子节点走。1* 记得要与之前遍历到的所有点的答案取最优,详见代码:

    cpp 复制代码
    int query(int p,int x) {
    	if(tr[p].l==tr[p].r) return tr[p].best ;
    	int mid=(tr[p].l+tr[p].r)>>1 ;
    	// 递归查询对应子树,并与当前节点的 best 比较,取更优者
    	if(x<=mid) return supermx(tr[p].best,query(ls,x),x) ;
    	else return supermx(tr[p].best,query(rs,x),x) ;
    }

主函数的实现

  • 最后我们终于实现了李超树的完全体,就只剩最后卑微的主函数部分了。在加入一个线段之前记得一定在存线段的数组里加入这个线段 !这个很容易忘记(个人感觉)。其它的需要注意的就只是强制在线了感觉不难直接看代码吧。

    cpp 复制代码
    int lst=0 ;
    int calcx(int x) {return (x+lst-1)%modx+1 ;}
    int calcy(int y) {return (y+lst-1)%mody+1 ;}
    
    signed main() {
    	int tot=0 ;
    	build(1,1,N) ;
    	int q; read(q) ; while(q--) {
    		int opt ; read(opt) ;
    		if(opt==1) {
    			int x0,y0,x1,y1 ; read(x0) ; read(y0) ; read(x1) ; read(y1) ;
    			x0=calcx(x0) ; x1=calcx(x1) ; y0=calcy(y0) ; y1=calcy(y1) ;
    			if(x0>x1) {swap(x0,x1) ; swap(y0,y1) ;}
    			lin[++tot]={x0,y0,x1,y1} ;
    			modify(1,x0,x1,tot) ;
    		}
    		else {
    			int x ; read(x) ; x=calcx(x) ;
    			lst=query(1,x) ;
    			printf("%d\n",lst) ;
    		}
    	}
    	
    	return 0 ;
    }

最后放一下完整代码

结合注释理解哦~ 综合时间复杂度 \(O(Q\times \log N)\)

cpp 复制代码
#include <bits/stdc++.h>
#define ls (p<<1) 
#define rs (p<<1|1)
using ll=long long ;
using namespace std ;
const int N=1e5+10,eps=1e-9,modx=39989,mody=1e9+1 ;

namespace fastIO {
	template<typename T> inline void read(T&x) {
		bool f=0 ; char c ; x=0 ;
		do { c=getchar() ; if(c=='-') f=1 ; }while(c<'0'||c>'9') ;
		while(c>='0'&&c<='9') {
			x=x*10+c-'0' ;
			c=getchar() ;
		}
		if(f) x=-x ;
	}
}
using namespace fastIO ;

namespace line {
	struct lines {int x0,y0,x1,y1 ;} lin[N];
	double fy(int id,int x) { // 求第 id 条线段在横坐标 x 处的纵坐标
		if(lin[id].x0==lin[id].x1) return max(lin[id].y0,lin[id].y1) ;
		return 1.0*(x-lin[id].x0)*(lin[id].y0-lin[id].y1)/(lin[id].x0-lin[id].x1)+lin[id].y0 ;
	}
	int cmp(double a,double b) { //注意精度问题
		if(a-b>eps) return 1 ;
		if(b-a>eps) return -1 ;
		return 0 ;
	}
	// 在横坐标 x 处,比较线段 a 和线段 b 谁更优
	int supermx(int a,int b,int x) {
		int opt=cmp(fy(a,x),fy(b,x)) ;
		return (opt==1||(opt==0&&a<b))?a:b ;
	}
} 
using namespace line ;

namespace Segtree {
	struct segtree {int l,r,best ;} tr[N<<4];
	void build(int p,int l,int r) {
		tr[p]={l,r,0} ;
		if(l==r) return ;
		int mid=(l+r)>>1 ;
		build(ls,l,mid) ; build(rs,mid+1,r) ;
	}
	void modify(int p,int l,int r,int id) {
		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)>>1 ;
			// 维护best
			if(supermx(id,tr[p].best,mid)==id) swap(id,tr[p].best) ;
			//如果新线段在左端点处更优,则递归插入左子树
			if(supermx(id,tr[p].best,tr[p].l)==id) modify(ls,l,r,id) ;
			//如果新线段在右端点处更优,则递归插入右子树
			if(supermx(id,tr[p].best,tr[p].r)==id) modify(rs,l,r,id) ;
			return ;
		}
		modify(ls,l,r,id) ;
		modify(rs,l,r,id) ;
	}
	// 查询横坐标 x 处的最优线段编号
	int query(int p,int x) {
		if(tr[p].l==tr[p].r) return tr[p].best ;
		int mid=(tr[p].l+tr[p].r)>>1 ;
		// 递归查询对应子树,并与当前节点的 best 比较,取更优者
		if(x<=mid) return supermx(tr[p].best,query(ls,x),x) ;
		else return supermx(tr[p].best,query(rs,x),x) ;
	}
}
using namespace Segtree ;

int lst=0 ;
int calcx(int x) {return (x+lst-1)%modx+1 ;}
int calcy(int y) {return (y+lst-1)%mody+1 ;}

signed main() {
	int tot=0 ;
	build(1,1,N) ;
	int q; read(q) ; while(q--) {
		int opt ; read(opt) ;
		if(opt==1) {
			int x0,y0,x1,y1 ; read(x0) ; read(y0) ; read(x1) ; read(y1) ;
			x0=calcx(x0) ; x1=calcx(x1) ; y0=calcy(y0) ; y1=calcy(y1) ;
			if(x0>x1) {swap(x0,x1) ; swap(y0,y1) ;}
			lin[++tot]={x0,y0,x1,y1} ;
			modify(1,x0,x1,tot) ;
		}
		else {
			int x ; read(x) ; x=calcx(x) ;
			lst=query(1,x) ;
			printf("%d\n",lst) ;
		}
	}
	
	return 0 ;
}

应用

终于终于,把我们的屎山打完了。但是这和我最开始说的斜率优化啥关系啊。然后应用就来了:读题。这道题的 \(O(n^2)\) 公式应该大家都能秒列出来吧,那我就直接写咯:(这里 \(dpi\) 表示已经修了前 \(i\) 个桥的最小花费,\(prei\) 是 \(w\) 的前缀和,然后 \(h\) 与题面的意思一样)

\dp\[i=dpj+(hi-hj)^2+prei-1-prej \]

按照斜率优化的方法我们二次化简式子,把 \(i\) 放前面,\(j\) 放后面:

\dp\[i=hi*hi+prei-1-2*hi*hj+hj*hj-prej+dpj \]

那就有了这样一条直线:

\y=-2\*h\[i*hj+hj*hj-prej+dpj \]

把这条直线加进李超树里就可以。注意这里加入的直线,所以我们直接加就可以。所以时间复杂度会少一个 \(\log\) 。详见代码:

cpp 复制代码
#include <bits/stdc++.h>
#define int long long
#define ls (p<<1) 
#define rs (p<<1|1)
//using ll=long long ;
using namespace std ;
const int N=1e6+10,inf=1e18 ;
int pre[N],h[N],dp[N] ;

namespace fastIO {
	template<typename T> inline void read(T&x) {
		bool f=0 ; char c ; x=0 ;
		do { c=getchar() ; if(c=='-') f=1 ; }while(c<'0'||c>'9') ;
		while(c>='0'&&c<='9') {
			x=x*10+c-'0' ;
			c=getchar() ;
		}
		if(f) x=-x ;
	}
}
using namespace fastIO ;

namespace lines {
	struct line {
		int k,b ;
		line(int kk=0,int bb=inf) {k=kk ; b=bb ;  }
	}lin[N] ;
	int fy(int id,int x) {return lin[id].k*x+lin[id].b ;}
	int supermin(int a,int b,int x) {
		return fy(a,x)<=fy(b,x)?a:b ;
	}
}
using namespace lines ;

namespace Segtree {
	struct segtree {int l,r,best ;} tr[N<<2] ;
	void build(int p,int l,int r) {
		tr[p]={l,r,0} ;
		if(l==r) return ;
		int mid=(l+r)>>1 ;
		build(ls,l,mid) ; build(rs,mid+1,r) ;
	}
	void modify(int p,int id) {
		int mid=(tr[p].l+tr[p].r)>>1 ;
		if(supermin(id,tr[p].best,mid)==id) swap(id,tr[p].best) ;
		if(tr[p].l==tr[p].r) return ;
		if(supermin(id,tr[p].best,tr[p].l)==id) modify(ls,id) ;
		else if(supermin(id,tr[p].best,tr[p].r)==id) modify(rs,id) ;
	}
	int query(int p,int x) {
		if(tr[p].l==tr[p].r) return tr[p].best ;
		int mid=(tr[p].l+tr[p].r)>>1 ;
		if(x<=mid) return supermin(tr[p].best,query(ls,x),x) ;
		else return supermin(tr[p].best,query(rs,x),x) ;
	}
}
using namespace Segtree ;

int fk(int i) {return -2ll*h[i] ;}
int fb(int j) {return h[j]*h[j]-pre[j]+dp[j] ;}

signed main() {
	build(1,0,1e6) ;
	int n ; read(n) ;
	for(int i=1;i<=n;i++) read(h[i]) ;
	for(int i=1,x;i<=n;i++) {read(x) ; pre[i]=pre[i-1]+x ;}
	lin[1]=line(fk(1),fb(1)) ;
	modify(1,1) ;
	dp[1]=0 ;
	for(int i=2;i<=n;i++) {
		int best=fy(query(1,h[i]),h[i]) ;
		dp[i]=best+h[i]*h[i]+pre[i-1] ;
		lin[i]=line(fk(i),fb(i)) ;
		modify(1,i) ;
	}
	printf("%lld",dp[n]) ;
	return 0 ;
}

// dp[i]=h[i]*h[i]+pre[i-1]-2*h[i]*h[j]+h[j]*h[j]-pre[j]+dp[j]

后记

其实李超树不算难吧,理解之后就真的很简单。多加练习即可。我写的感觉还算清晰吧?希望吧。 我码了3天我哭了,无耻求赞~