斜率优化学习笔记

掌握了斜率优化dp,就可以去切一堆紫题了

斜率优化dp原理:利用数学的斜率,通过计算查找得到最优的斜率以及常数项使得答案最优

维护最佳答案选择,我们通常都要维护一个上凸包或者一个下凸包

怎么才是一个优秀的凸包?

只需要斜率单调递增/递减即可

那么我们的这个斜率怎么求?

y与x的比值,分别为常数以及带乘法的类似系数的玩意

具体怎么分析还是需要列式子

实在不行可以把大于号小于号都试试

下面说几种常规的 \(O(n^2)\) 通过斜率优化优化到不同时间复杂度的例子

Building Bridges

\(O(n\log^2n)\)

一道泛用性较高的斜率优化dp,但也会比较难写

可得式子

\[dp[i]=dp[j]+(h[i]-h[j])^2+pre[i-1]-pre[j] \]

可以化为

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

此时把只和 \(i\) 有关的都放到一边可以得到式子

\(dp[i]-h[i]*h[i]-pre[i-1]=dp[j]+h[j]*h[j]-2*h[i]*h[j]-pre[j]\)

那么此时y为仅仅与 \(j\) 有关的

而x为\(h[j]\)

此时我们先维护一个凸包,然后进行查询

首先维护凸包,因为每一次的加入的x大小关系不同,我们需要考虑不同的解法

第一种CDQ分治

每次分治在左边建一个凸包,然后给右边查询即可

第二种二进制分组,按照每个二进制位的大小进行凸包的维护,每一次更新都是 \(\log n\) 的,然后我们再进行枚举位置,取最值即可

然后查询时,使用二分查找最优情况即可

CDQ:

#include<bits/stdc++.h>
#define int long long
using namespace std;
/*
dp[i]=min(dp[j]+(h[i]-h[j])^2+pre[i-1]-pre[j])
dp[j]+h[j]*h[j]-pre[j]
-2*h[j]
*/
int n,dp[100005],pre[100005],s[100005],p[100005],h[100005],top;
struct P{
	int x,y;
}a[100005],tmp[100005];
bool cmp(P a,P b){
	if(a.x!=b.x)return a.x<b.x;
	return a.y<b.y;
}
bool slope(int i,int j,int k){
	return (tmp[i].y-tmp[j].y)*(tmp[j].x-tmp[k].x)<=(tmp[j].y-tmp[k].y)*(tmp[i].x-tmp[j].x);
}
int calc(int i,int j){
	return tmp[j].y+h[i]*h[i]+pre[i-1]-2*h[i]*tmp[j].x;
}
int find(int x){
	int l=1,r=top;
	while(l<r){
		int mid=l+r>>1;
		if(calc(x,p[mid])>calc(x,p[mid+1]))l=mid+1;
		else r=mid;
	}
	return p[l];
}
void CDQ(int l,int r){
	if(l==r)return;
	int mid=l+r>>1;
	CDQ(l,mid);
	for(int i=l;i<=r;i++)tmp[i]=a[i];
	sort(tmp+l,tmp+mid+1,cmp);
	top=0;
	for(int i=l;i<=mid;i++){
		while(top>1&&slope(i,p[top],p[top-1]))top--;
		p[++top]=i;
	}
	for(int i=mid+1;i<=r;i++){
		dp[i]=min(dp[i],calc(i,find(i)));
		a[i]={h[i],dp[i]+h[i]*h[i]-pre[i]};
	}
	CDQ(mid+1,r);
}
signed main(){
	memset(dp,0x3f,sizeof(dp));
	dp[1]=0;
	cin>>n;
	for(int i=1,x;i<=n;i++)cin>>h[i];
	for(int i=1;i<=n;i++)cin>>s[i];
	for(int i=1;i<=n;i++)pre[i]=pre[i-1]+s[i];
	a[1]={h[1],h[1]*h[1]-pre[1]};
	CDQ(1,n);
	cout<<dp[n];
	return 0;
}

二进制分组:

#include<bits/stdc++.h>
#define int long long
using namespace std;
/*
dp[i]=dp[j]+(h[i]-h[j])^2+pre[i-1]-pre[j]
dp[i]=dp[j]+h[i]*h[i]+h[j]*h[j]-2*h[i]*h[j]+pre[i-1]-pre[j]
*/
struct P{
	int x,y;
}a[100005];
int dp[100005],n,h[100005],w[100005],pre[100005],lg[200005];
bool cmp(P a,P b){
	if(a.x!=b.x)return a.x<b.x;
	return a.y<b.y;
}
bool slope(int i,int j,int k){
	return (a[i].y-a[j].y)*(a[j].x-a[k].x)<=(a[j].y-a[k].y)*(a[i].x-a[j].x);
}
int calc(int i,P j){
	return j.y+j.x*h[i]+h[i]*h[i]+pre[i-1];
}
int lowbit(int x){
	return x&-x;
}
struct CH{
	int p[100005],top;
	void build(int l,int r){
		top=0;
		for(int i=l;i<=r;i++)a[i]={-2*h[i],h[i]*h[i]-pre[i]+dp[i]};
		sort(a+l,a+r+1,cmp);
		for(int i=l;i<=r;i++){
			while(top>1&&slope(i,p[top],p[top-1]))top--;
			p[++top]=i;
		}
	}
	int query(int k){
		int l=1,r=top;
		while(l<r){
			int mid=l+r>>1;
			if(calc(k,a[p[mid]])>=calc(k,a[p[mid+1]]))l=mid+1;
			else r=mid;
		}
		return calc(k,a[p[l]]);
	}
}c[25];
signed main(){
	for(int i=1;i<=17;i++)lg[(1<<i-1)]=i;
	memset(dp,0x3f,sizeof(dp));
	dp[1]=0;
	cin>>n;
	for(int i=1;i<=n;i++)cin>>h[i];
	for(int i=1;i<=n;i++)cin>>w[i];
	for(int i=1;i<=n;i++)pre[i]=pre[i-1]+w[i];
	for(int i=2;i<=n;i++){
		int j=lowbit(i-1);
		c[lg[j]].build(i-j,i-1);
		for(int tmp=i-1;tmp;tmp-=j,j=lowbit(tmp))dp[i]=min(dp[i],c[lg[j]].query(i));
	}
	cout<<dp[n];
	return 0;
}

虽然泛用性强但是又慢还长

但是在其它题目中可能会更简单

比如x是单调递增的,那么可以考虑直接开个队列跑

那么此时时间复杂度为 \(O(n\log n)\)

没有找到板题

还有 \(O(n)\) 的方法

对于最优决策点一定后移的情况,可以直接上单调队列

羊羊列队

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,a[10005],x[10005],y[10005],dp[10005],p[10005],head,tail;
/*
dp[i][j]=min(dp[k][j-1]+(a[i]-a[k+1])^2)
dp[i][j]=min(dp[k][j-1]+a[k+1]^2-2*a[i]*a[k+1])+a[i]^2
dp[x]+a[x+1]^2-2*a[i]*a[x+1]<=dp[y]+a[y+1]^2-2*a[i]*a[y+1]
y_x-y_y / x_x-x_y <= a[i]
*/
bool slope(int i,int j,int k){
	return (y[i]-y[j])*(x[j]-x[k])<=(y[j]-y[k])*(x[i]-x[j]);
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	sort(a+1,a+n+1);
	memset(dp,0x3f,sizeof(dp));
	dp[0]=0;
	for(int i=1;i<=m;i++){
		for(int j=0;j<=n;j++)x[j]=2*a[j+1],y[j]=dp[j]+a[j+1]*a[j+1];
		head=1,tail=0;
		p[++tail]=i-1;
		for(int j=i;j<=n;j++){
			while(head<tail&&y[p[head+1]]-y[p[head]]<=a[j]*(x[p[head+1]]-x[p[head]]))head++;
			dp[j]=y[p[head]]-a[j]*x[p[head]]+a[j]*a[j];
			while(head<tail&&slope(j,p[tail],p[tail-1]))tail--;
			p[++tail]=j;
		}
	}
	cout<<dp[n];
	return 0;
}

那么这斜率优化好像有一点水,能不能上一点点强度?

那是当然有了

首先出场的是 谷仓

虽然只是板子,但是需要拆环枚举

代码:

/*
dp[j]=min(dp[k]+j*(pre[j]-pre[k])-s[j]+s[k])
x<y,y优
dp[x]-i*pre[x]+s[x]>dp[y]-i*pre[y]+s[y]
dp[x]-dp[y]+s[x]-s[y]>(pre[x]-pre[y])*i
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, m, r[1005], pre[1005], s[1005], ans = 1e16, dp[1005], p[1005], head, tail, x[1005], y[1005];
bool check(int i, int j, int k) { return (y[i] - y[j]) * (x[j] - x[k]) <= (y[j] - y[k]) * (x[i] - x[j]); }
void solve() {
    for (int i = 1; i <= n; i++) pre[i] = pre[i - 1] + r[i];
    for (int i = 1; i <= n; i++) s[i] = s[i - 1] + r[i] * i;
    memset(dp, 0x3f, sizeof(dp));
    dp[0] = 0;
    for (int i = 1; i <= m; i++) {
        head = 1, tail = 0;
        p[++tail] = 0;
        for (int j = 1; j <= n; j++) y[j] = dp[j] + s[j], x[j] = pre[j];
        for (int j = 1; j <= n; j++) {
            while (head < tail && y[p[head]] - y[p[head + 1]] > j * (x[p[head]] - x[p[head + 1]])) head++;
            dp[j] = y[p[head]] - x[p[head]] * j + j * pre[j] - s[j];
            while (head < tail && check(j, p[tail], p[tail - 1])) tail--;
            p[++tail] = j;
        }
    }
    ans = min(ans, dp[n]);
}
signed main() {
    cin >> n >> m;
    for (int i = n; i >= 1; i--) cin >> r[i];
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j < n; j++) r[j] = r[j + 1];
        r[n] = r[0];
        solve();
    }
    cout << ans;
    return 0;
}

接下来是 Harbingers

你说的对,但是那时我好像还没有学过树形dp

直接手写一个单调栈,然后记录头部,记得还原即可

代码:

/*
dp[i]=min(dp[j]+(l[i]-l[j])*v[i])+s[i]
x<y
y better
dp[x]-l[x]*v[i]>dp[y]-l[y]*v[i]
Y[x]=dp[x]
X[x]=l[x]
Y[x]-Y[y]>v[i]*(X[x]-X[y])
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, m, st[100005], top, s[100005], v[100005], dp[100005], x[100005], y[100005], l[100005], b[100005];
struct P {
    int x, y;
};
vector<P> e[100005];
bool check(int i, int j, int k) {
    return (__int128)(y[i] - y[j]) * (x[j] - x[k]) <= (__int128)(y[j] - y[k]) * (x[i] - x[j]);
}
int calc(int i, int j) { return dp[j] + (l[i] - l[j]) * v[i] + s[i]; }
pair<int, int> push(int x) {
    int now = top, tmp, l = 1, r = top;
    while (l < r) {
        int mid = l + r >> 1;
        if (check(x, st[mid + 1], st[mid]))
            r = mid;
        else
            l = mid + 1;
    }
    top = l;
    tmp = st[++top], st[top] = x;
    return { now, tmp };
}
void reset(int x, int y) {
    st[top] = y;
    top = x;
}
int find(int v) {
    int l = 1, r = top;
    while (l < r) {
        int mid = l + r >> 1;
        if (calc(v, st[mid]) >= calc(v, st[mid + 1]))
            l = mid + 1;
        else
            r = mid;
    }
    return st[l];
}

void dfs(int p, int fa, int sum) {
    l[p] = sum;
    dp[p] = calc(p, find(p));
    x[p] = l[p];
    y[p] = dp[p];
    pair<int, int> tmp2 = push(p);
    for (P i : e[p])
        if (i.x != fa)
            dfs(i.x, p, sum + i.y);
    reset(tmp2.first, tmp2.second);
}
signed main() {
    cin >> n;
    for (int i = 1, a, b, c; i < n; i++) {
        cin >> a >> b >> c;
        e[a].push_back({ b, c });
        e[b].push_back({ a, c });
    }
    for (int i = 2; i <= n; i++) cin >> s[i] >> v[i];
    dfs(1, 0, 0);
    for (int i = 2; i <= n; i++) cout << dp[i] << ' ';
    return 0;
}

市场监控来了

太好啦是斜率优化,我们有救啦

完啦是分块,我们又没救啦

到底是哪个人才想到在分块里面维护凸包来斜优的

调代码时长两年半

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,B,id[100005],L[405],R[405];
struct P{
	int x,y,id;
	bool friend operator<(P a,P b){
		if(a.x!=b.x)return a.x<b.x;
		return a.y<b.y;
	}
}a[100005];
bool slope(P i,P j,P k){
	return (__int128)(i.y-j.y)*(j.x-k.x)>=(__int128)(j.y-k.y)*(i.x-j.x);
}
struct CV{
	int top,len;
	P b[405],p[405];
	void build(){
		top=0;
		for(int i=1;i<=len;i++){
			while(top>1&&slope(b[i],p[top],p[top-1]))top--;
			p[++top]=b[i];
		}
	}
	void change(int w,P v){
		bool f=0;
		int q;
		for(int i=1;i<=len;i++){
			if(b[i].id==w){
				f=1;
				q=i;
				b[i]=v;
				break;
			}
		}
		if(!f){
			b[++len]=v;
			q=len;
		}
		while(q<len&&!(b[q]<b[q+1])){
			swap(b[q],b[q+1]);
			q++;
		}
		while(q>1&&b[q]<b[q-1]){
			swap(b[q],b[q-1]);
			q--;
		}
		build();
	}
	int query(int x){
		int l=1,r=top;
		if(!top)return -1e16;
		while(l<r){
			int mid=l+r>>1;
			if(p[mid].y-p[mid+1].y<(p[mid+1].x-p[mid].x)*x)l=mid+1;
			else r=mid;
		}
		return p[l].x*x+p[l].y;
	}
}st[405];
void init(){
	B=sqrt(n)+1;
	for(int i=1;i<=n;i++){
		int x=(i-1)/B+1;
		id[i]=x;
		if(!L[x])L[x]=i;
		R[x]=i;
	}
}
int query(int k,int l,int r){
	int ans=-1e16;
	if(id[l]==id[r]){
		for(int i=l;i<=r;i++)if(a[i].id)ans=max(ans,a[i].x*k+a[i].y);
		return ans;
	}
	for(int i=l;i<=R[id[l]];i++)if(a[i].id)ans=max(ans,a[i].x*k+a[i].y);
	for(int i=id[l]+1;i<id[r];i++)ans=max(ans,st[i].query(k));
	for(int i=L[id[r]];i<=r;i++)if(a[i].id)ans=max(ans,a[i].x*k+a[i].y);
	return ans;
}
signed main(){
	cin>>n>>m;
	init();
	int op,t,k,s,z,l,r;
	while(m--){
		cin>>op;
		if(op==1){
			cin>>t>>k>>z>>s;
			a[k]={z,s-z*t,k};
			st[id[k]].change(k,a[k]);
		}
		else {
			cin>>t>>l>>r;
			if(l>r)swap(l,r);
			int tmp=query(t,l,r);
			if(tmp==-1e16)puts("nema");
			else cout<<tmp<<endl;
		}
	}
	return 0;
}

完结撒花

posted @ 2025-11-28 10:07  huhangqi  阅读(0)  评论(0)    收藏  举报
/*
*/