(笔记)斜率优化 DP 李超线段树

下午没什么事,听 Lgx_Q 神犇的课又听不懂,遂补个 blog。

斜率优化 DP

单调队列维护凸包

这是一类较为经典的 DP 优化问题。

具体来说,它们都具有这样的转移形式:

\[f_i=\min_{j<i}\{f_j+a_ib_j+c_i+d_j\} \]

它的转移里面有一项和 \(i,j\) 都有关。处理这类问题的方式就是先把 \(\min\)(或者 \(\max\))拆掉,然后把 \(j\) 看成是常数,式子整理成 \(y=kx+b\) 的形式,\(b,k\) 只和 \(i\) 有关,\(y,x\) 只和 \(j\) 有关。这样我们要求的 \(f_i\) 就一定在 \(b=y-kx\) 里面,令该值最大/最小化即可。这就是斜率优化 DP 截距的来源。

每次转移的时候,只需要在某种结构里面记录 \((x,y)\),然后找出当前对应 \(k\) 下最大化/最小化的截距 \(b\) 即可。

具体来说,如果 \(b_j\) 具有单调性,那么仍可以用单调队优化,每次记录 \((x,y)\) 往队尾(或队头)丢个值即可。甚至于当 \(k\) 不单调时,你也可以通过在单调队列上二分找出最优解。

但是万一 \(b_j\) 也不单调,这时你就老实了,这里我们下文会介绍一种解决方法叫李超线段树,在 \(O(n\log n)\) 的时间内解决这个问题。

例题

P3195 [HNOI2008] 玩具装箱

转移:

\[\begin{aligned} pre_n&=\sum_{i=1}^nC_i\\ A_n&=n+pre_n\\ B_n&=n+pre_{n-1}\\ x&=A_i-B_j\\ f_i&=\min_{j<i}\{f_j+L^2+A_i^2+B_j^2-2A_iB_j-2A_iL+2B_jL\}\\ f_i-L^2-A_i^2+2A_iL&=f_j+B_j^2+2B_jL-2A_iB_j\\ b&=f_i-L^2-A_i^2+2A_iL\\ y&=f_j+B_j^2+2B_jL\\ k&=2A_i\\ x&=B_j\implies\\ b&=y-kx \end{aligned} \]

这里容易观察到 \(A_i,B_j\) 都具有单调性,我们考虑找最优决策点。我们存下来的 \((x,y)\) 点集应该构成一个凸包,在这里我们每次在队尾加入点,每次改变斜率 \(k\) 去截这个凸包上的点,要使截出来的截距最大化。

image

这是上凸包的形态。

image

这是下凸包的形态。

直线斜率(正)递增,所以动态处理应该是不停在逆时针旋转的。我们要取到的是最优决策点 \(A\),为了让接下来的决策也能取到可能的最优决策点,我们维护一个下凸包。截到的最优的点 \(A\),想象一下,一定在该凸包与直线的斜率拟合处。由于斜率(正)递增,有些队列前面的节点永远取不到了,这时候我们可以把队头的一些没用的节点扔掉,这些节点比它们右边的节点截出来的东西严格更劣。那就变成了这样:

image

然后我们再考虑在队尾插入一些节点,这些都建立在单调性的基础上。如插入一个节点 \(B\),我们需要不断地扔掉队尾的点直到加入节点 \(B\) 不会破坏下凸包。为什么这样是对的?考虑我们加入 \(B\) 破坏了下凸包结构的状态,那么此时直线拟合到队尾那个点的时候决策一定更劣(感性理解)。再者,如果发生冲突,为什么不能保留队尾而是要保留最新的 \(B\)?因为对于到达那里的直线,截取 \(B\) 一定要比截取旧队尾要更优。

代码实现:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<LL,LL> PII;
const int N=5e4+5;
LL n,L,c[N],pre1[N],pre2[N];
struct dq{
	PII val[N];
	int hd=1,tl;
	void push(PII a){val[++tl]=a;}
	void pop_back(){tl--;}
	void pop_front(){hd++;}
	int siz(){return tl-hd+1;}
	PII front(){return val[hd];}
	PII back(){return val[tl];}
	PII sfront(){return val[hd+1];}
	PII sback(){return val[tl-1];}
}q;
LL dp[N];
LL gt(PII a,LL k){return a.second-k*a.first;}
bool cmp(PII a,PII b,LL k){return gt(a,k)>=gt(b,k);}
bool bet(PII a,PII b,PII c){
	return 1.0*(b.second-a.second)*(c.first-a.first)>1.0*(c.second-a.second)*(b.first-a.first);
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>L;
	for(int i=1;i<=n;i++){
		cin>>c[i];
		c[i]+=c[i-1];
		pre1[i]=c[i]+i;
		pre2[i]=c[i]+i+L+1;
	}
	q.push(make_pair((L+1),(L+1)*(L+1)));
	for(int i=1;i<=n;i++){
		while(q.siz()>=2&&cmp(q.front(),q.sfront(),2*pre1[i]))q.pop_front();
		dp[i]=gt(q.front(),2*pre1[i])+pre1[i]*pre1[i];
		PII id=make_pair(pre2[i],pre2[i]*pre2[i]+dp[i]);
		while(q.siz()>=2&&bet(q.sback(),q.back(),id)){
			q.pop_back();
		}
		q.push(id);
	}
	cout<<dp[n];
	return 0;
}

李超线段树

解决的是在平面直角坐标系中,加入一些线段 \(y=kx+b(x\in [A,B])\),然后询问 \(x\in[A,B]\) 中取到的 \(y\) 最大/最小值是多少。基本思想是用线段树来拆开找到查询/修改的所有完整子区间,然后再在这些区间里面操作。具体地,每个区间都标上一个编号为 \(tg_p\) 的线段,表示在节点 \(p\) 所代表的区间 \([l,r],mid=\lfloor\frac{l+r}{2}\rfloor\)\(x=mid\) 取到的 \(y\) 最大值。如果我们丢进来一条线段,找到一个空节点 \(tg_p=0\),就直接挂在 \(tg_p\) 上。如果它在 \(x=mid\) 的时候比原线段更劣,那我们考虑这两个线段的交点在哪里。如果在左区间就往左区间递归,反之亦然。如果都不在,说明被完全偏序了,直接丢掉即可。如果更优,那么直接进行 \(tg_p\) 和新线段的交换,然后把原来的 \(tg_p\) 当作更劣情况继续递归,时间复杂度 \(O(n\log^2n)\)

具体地,在斜率优化一类问题中,我们不用分割这些区间,而是直接在全局上做,不用考虑线段端点的问题,所以时间复杂度是 \(O(n\log n)\)的。我们的具体应用是把加入的点 \((x,y)\)\(-x\) 当作斜率,\(y\) 当作截距,\((k,b)\) 作为取到的值与对应点值。这个应用和单调队列优化是相反的,但是更加自然。

这里给出模板题 P4097 【模板】李超线段树 / [HEOI2013] Segment 的代码实现:

#include<bits/stdc++.h>
typedef double LD;
using namespace std;
const int N=1e5+10;
const int mod=39989,MOD=1e9;
int n,op,lst;
const LD eps=1e-9;
struct Seg{
	LD k,b;
	LD val(int x){
		return 1.0*k*x+1.0*b;
	}
}s[N];
Seg Sg(int xa,int ya,int xb,int yb){
	Seg ans;
	if(xa==xb){
		ans.k=0;
		ans.b=1.0*max(ya,yb);
	}
	else {
		ans.k=(yb-ya)*1.0/(xb-xa);
		ans.b=ya-1.0*ans.k*xa;
	}
	return ans;
}
int cmp(LD x,LD y){
	if(y-x>eps)return 0;
	if(x-y>eps)return 1;
	return 2;
}
int mx(int x,int y,int v){
	int op=cmp(s[x].val(v),s[y].val(v));
	if(op==2)return (x<y?x:y);
	return (op ? x : y);
}
int idx;
struct Sgt{
	#define ls (p<<1)
	#define rs (p<<1|1)
	#define mid ((l+r)>>1)
	int tg[N<<2];
	void update(int p,int l,int r,int val){
		if(!tg[p]){tg[p]=val;return ;}
		if(mx(tg[p],val,mid)==val)swap(tg[p],val);
		if(l==r)return ;
		if(mx(tg[p],val,l)==val)update(ls,l,mid,val);
		if(mx(tg[p],val,r)==val)update(rs,mid+1,r,val);
	}
	void fids(int p,int l,int r,int L,int R,int val){
		if(L<=l&&r<=R){update(p,l,r,val);return ;}
		if(L<=mid)fids(ls,l,mid,L,R,val);
		if(R>mid)fids(rs,mid+1,r,L,R,val);
	}
	int que(int p,int l,int r,int x){
		if(l==r)return tg[p];
		if(x<=mid)return mx(tg[p],que(ls,l,mid,x),x);
		else return mx(tg[p],que(rs,mid+1,r,x),x);
	}
	#undef ls
	#undef rs
	#undef mid
}T;
const int siz=39990;
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	while(n--){
		cin>>op;
		if(op){
			int xa,xb,ya,yb;
			cin>>xa>>ya>>xb>>yb;
			xa=(xa+lst-1)%mod+1;
			ya=(ya+lst-1)%MOD+1;
			xb=(xb+lst-1)%mod+1;
			yb=(yb+lst-1)%MOD+1;
			if(xa>xb)swap(xa,xb),swap(ya,yb);
			s[++idx]=Sg(xa,ya,xb,yb);
			T.fids(1,1,siz,xa,xb,idx);
		}
		else {
			int k;
			cin>>k;
			k=(k+lst-1)%mod+1;
			lst=T.que(1,1,siz,k);
			cout<<lst<<'\n';
		}
	}
	return 0;
}
posted @ 2025-05-04 16:50  TBSF_0207  阅读(23)  评论(0)    收藏  举报