【动态规划 进阶】斜率优化dp 学习总结

斜率优化dp也是针对一类固定的动态规划类型所作出的类型判断。


在对斜率优化做出详细解释之前,我们先来看一道经典的例题:
luogu P2365 任务安排

0x00 引子

当我们初次观察这道题时,我们发现这道题的数据范围并不是非常大,
0 < = n < = 5000 0 <=n <= 5000 0<=n<=5000
0 < = s < = 50 0 <= s<=50 0<=s<=50
当我们观察这道题,会很直观地想到状态转移方程:
F [ i , j ] = min ⁡ F [ k , j − 1 ] + ( S ∗ j + s u m T [ i ] ∗ ( s u m C [ i ] − s u m C [ k ] ) ) ∣ 0 < = k < i F[i , j] = \min{F[k , j-1]+(S*j+sumT[i]*(sumC[i]-sumC[k]))}|0<=k<i F[i,j]=minF[k,j1]+(Sj+sumT[i](sumC[i]sumC[k]))∣0<=k<i
F[i,j]表示在前i个数中分成j个任务块所得到的最小花费。
然而我们发现,这道题并不必须对F[][]使用二维数组,因为我们有一个新的思想:
费用提前计算。


简单介绍一下费用提前计算:
由于动态规划最基本的思想,针对于一些因素影响的最值,我们可以尽可能的求出他的最小未知代价。例如此题,我们在只知道从1到i个物品分成j个处理段的情况下,代价最小就是剩下的物品都放入一个处理段中。因此,解决这类问题的方法就是:
F [ i ] = min ⁡ ( F [ j ] + s u m T [ i ] ∗ ( s u m C [ i ] − s u m C [ j ] ) + S ∗ ( s u m C [ N ] − s u m C [ j ] ) ∣ 0 < = j < i F[i]=\min(F[j]+sumT[i]*(sumC[i]-sumC[j])+S*(sumC[N]-sumC[j])|0<=j<i F[i]=min(F[j]+sumT[i](sumC[i]sumC[j])+S(sumC[N]sumC[j])∣0<=j<i
此时的代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std ;

const int MAXN = 5005 ;

int n ,s ;
long long sumt[MAXN] ;
long long sumc[MAXN] ;
long long f[MAXN] ;

int main(){
    memset(f , 0x3f , sizeof(f)) ;
    
    scanf("%d%d" , &n , &s) ;
    for(int i = 1;i <= n;++i){
        scanf("%lld" , sumt+i) ;
        sumt[i] += sumt[i-1] ;
        scanf("%lld" , sumc+i) ;
        sumc[i] += sumc[i-1] ;
    }
    
    f[0] = 0 ;
    for(int i = 1;i <= n;++i)
            for(int k = 0;k < i;++k){//枚举k+1~i个任务在同一批中完成
                f[i] = min(f[i] , f[k] + sumt[i] * (sumc[i] - sumc[k]) + s * (sumc[n] - sumc[k])) ;
            //“费用提前计算”思想,说白了就是因为已经知道后面至少需要乘1次,所以提前先乘出来
			}
    
    printf("%lld" , f[n]) ;
    return 0 ;
}

然而 ,如果我们的数据范围再次扩大一点,变成了
1 < = n < = 3 ∗ 1 0 5 1 <=n<=3*10 ^ 5 1<=n<=3105
这种算法很显然会超时。
这时候就要用到一种方式进行优化时间
这时候我们就要用到 斜率优化

0x10 什么是用斜率优化

很显然,对于某类问题,我们必须要进行一定的操作,这是优化的特点
就像单调队列针对滑动窗口、区间最值一样。
那我们一定要找到 斜率优化 的特点或者说性质才可以吧
将上面的状态转移方程 简化一下

F [ i ] = min ⁡ ( F [ j ] + s u m T [ i ] ∗ ( s u m C [ i ] − s u m C [ j ] ) + S ∗ ( s u m C [ N ] − s u m C [ j ] ) F[i]=\min(F[j]+sumT[i]*(sumC[i]-sumC[j])+S*(sumC[N]-sumC[j]) F[i]=min(F[j]+sumT[i](sumC[i]sumC[j])+S(sumC[N]sumC[j])
F [ i ] = F [ j ] + s u m T [ i ] ∗ ( s u m C [ i ] − s u m C [ j ] ) + S ∗ ( s u m C [ N ] − s u m C [ j ] ) F[i] =F[j] + sumT[i]*(sumC[i]-sumC[j])+S*(sumC[N]-sumC[j]) F[i]=F[j]+sumT[i](sumC[i]sumC[j])+S(sumC[N]sumC[j])
F [ i ] = F [ j ] + s u m T [ i ] ∗ s u m C [ i ] − s u m T [ i ] ∗ s u m C [ j ] + S ∗ s u m C [ N ] − S ∗ s u m C [ j ] F[i]=F[j]+sumT[i]*sumC[i]-sumT[i]*sumC[j]+S*sumC[N]-S*sumC[j] F[i]=F[j]+sumT[i]sumC[i]sumT[i]sumC[j]+SsumC[N]SsumC[j]
F [ j ] = ( S + s u m T [ i ] ) ∗ s u m C [ j ] + F [ i ] − s u m T [ i ] ∗ s u m C [ i ] − S ∗ s u m C [ n ] F[j]=(S+sumT[i])*sumC[j]+F[i]-sumT[i]*sumC[i]-S*sumC[n] F[j]=(S+sumT[i])sumC[j]+F[i]sumT[i]sumC[i]SsumC[n]

这就是简化过程,会发现最后将式子化成了一个很像
k = Δ y Δ x k=\tfrac {Δy}{Δx} k=ΔxΔy
的式子
k = ( f [ q [ t a i l ] ] − f [ q [ t a i l − 1 ] ] ) ( s u m c [ q [ t a i l ] ] − s u m c [ q [ t a i l − 1 ] ] ) ) k=\tfrac{(f[q[tail]] - f[q[tail-1]])}{(sumc[q[tail]] - sumc[q[tail-1]]))} k=(sumc[q[tail]]sumc[q[tail1]]))(f[q[tail]]f[q[tail1]])

至此,我们终于引入了“斜率”与dp的关系
这也是为什么要用“斜率”去优化dp的原因
那就有读者说了
是所有式子都可以化为“斜率”吗
很显然不是
针对一部分具有特殊模样的式子才能变成这样的斜率式
这种式子基本模型:
f [ i ] = min ⁡ ( f [ j ] + v a l ( i , j ) ) f[i] = \min{(f[j]+val(i,j))} f[i]=min(f[j]+val(i,j))
其中的 val 表示状态转移中的利润(或代价)
而很明显 上面的式子是符合这种条件的。
因此要使用这种方法。

0x30 如何使用斜率优化?

笔者将他分成了几个步骤:
1.想尽各种手段简化朴素状态转移方程(例如 前缀和 、滚动数组)
2.展开优化后的状态转移方程
3.找到可以被其他字母代替的因子式(字母指 i , j , k;因子式指sum[i] , i , d[j])作为 x
4.分别将两个字母化作不等式的两边(原本等号两边的可以不去管它,会消掉)
5.化简不等式,并改变成斜率的形式 (delta y / delta x ) 其中上面的delta是 y
6.按照 x 和 原展开后的状态转移方程找到 k ,剩下的都是 b

0x31 使用斜率式

变成了斜率式,也就是上面我们提到的 delta y / delta x ,我们下一步就是要使用他。使用斜率式最本质的点就是 去掉根本不可成为最优解的值
在使用斜率之前,我们先要检查题目的决策单调性。说白了,就是看他是不是凸包。
在之前讲单调队列优化dp时,我们就说过单调队列对单调性的要求。
但是斜率优化题不可以用单调队列就是因为他拥有两个维度,而恰恰时有两个维度,才可以将这两个维度转换成另一个新的维度:斜率维度。就这样,这就解释了为什么斜率可以解决单调队列优化dp解决不了的题了。
当然,有一部分题不具备单调性,还可以用二分答案来解决(例如多重背包Ⅲ)我们暂且不提。
解决了这些问题,我们也就会用斜率式了:不过是单调队列dp转换成了斜率嘛!

0x32 维护dp关系

这里选择单调队列维护dp。
有读者会感到疑惑,为什么这里的单调队列不符合 滑动窗口类型 呢
其实是复合的。当我们观察是否需要删去节点时,我们世纪上只用到了3个点:a , b , c,观察 b 是否要去除。
在这里插入图片描述
图片来自网络,侵删,大家看看即可。
在这里插入图片描述

0x33 寻找最优决策点

最优点往往是最优斜率的直线由负半轴不断向上平移产生的(直到相切)。
这是大部分blog的解释。
但是我认为单调队列也好,斜率优化也好,这只是寻找答案的工具,寻找我们需要的答案的工具。
试问我们需要什么?因题而异。本题中要找单调增的斜率,那自然是越来越靠上的越合适。
当我们想到这,woc,这不就是出队条件吗?

0x34 思考:斜率优化 与 单调队列优化 的关系与不同

总结一下所有的不同点:
1.单调队列具有单调性,但只能维护1维数组
斜率优化可以利用状态降维实现由2~3维降到1维,再用单调队列优化
2.单调队列具有基本的实现操作,不会过于复杂
斜率优化基于朴素状态转移方程,进行一定的斜率转换,有其他算法可以用来维护。
(例如 wqs二分、二分找最优斜率等)

0x40 具体实现

code:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std ;

const int MAXN = 3e5 + 5 ;

int n ,s ;
long long sumt[MAXN] ;
long long sumc[MAXN] ;
long long f[MAXN] ;
long long q[MAXN] ;
int head = 1 ;
// int tail = 1 ;
int tail = 1 ;

int main(){
    scanf("%d%d" , &n , &s) ;
    for(int i = 1;i <= n;++i){
        scanf("%lld" , sumt + i) ;
        sumt[i] += sumt[i-1] ;
        scanf("%lld" , sumc + i) ;
        sumc[i] += sumc[i-1] ;
    }
    
    memset(f , 0x3f , sizeof(f)) ;
    f[0] = 0 ;
    q[1] = 0 ;
    
    for(int i = 1;i <= n;++i){
        //清除不合法队首元素
        while(head < tail &&
        // while(head <= tail &&
             (f[q[head+1]] - f[q[head]]) <= (s + sumt[i]) * (sumc[q[head+1]] - sumc[q[head]]))
            head++ ;
            
        //计算状态转移
        f[i] = f[q[head]] - (s + sumt[i]) * sumc[q[head]] + sumt[i] * sumc[i] + s * sumc[n] ;
            
        //清除不合法队尾元素
        while(head < tail && 
        // while(head <= tail &&
             (f[q[tail]] - f[q[tail-1]]) * (sumc[i] - sumc[q[tail]]) >= 
             (f[i] - f[q[tail]]) * (sumc[q[tail]] - sumc[q[tail-1]]))
            tail-- ;
        
        //入队
        q[++tail] = i ;
    }
    
    printf("%lld" , f[n]) ;
    return 0 ;
}

0x50 例题分析

0x51 luogu CF311B Cats Transport

突然发现好像所有斜率优化的题都长得很像 莫名其妙?
说不出来的像
例如本道题与 luogu P4360 P2120
好像都是有关于 从左推到右,过程有代价的题
注意这里的从左推到右有可能是抽象的
例如 luogu P4072 [SDOI2016]征途
code:

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;

const int MX = 1e5+10 ;

long long d[MX],t[MX],S[MX];
int q[101][MX];
long long dp[101][MX];
int hd[MX],tl[MX];

double Slope(int i,int j1,int j2) {
	return (1.0 * S[j1] + dp[i][j1] - S[j2] - dp[i][j2]) / (j1 - j2);
}

int main() {
	int n,m,p;
	cin >> n >> m >> p;
	for(int i = 2 ; i <= n ; ++i) {
		cin >> d[i];
		d[i] += d[i - 1];
	}
	for(int i = 1,x,p ; i <= m ; ++i) {
		cin >> p >> x;
		t[i] = -d[p] + x;
	}
	sort(t + 1,t + 1 + m);
	for(int i = 1 ; i <= m ; ++i)
		S[i] = S[i - 1] + t[i];
	for(int i = 1 ; i <= m ; ++i) {
		for(int j = 1 ; j <= min(i,p) ; ++j) {
			while(hd[j - 1] < tl[j - 1] && Slope(j - 1,q[j - 1][hd[j - 1]],q[j - 1][hd[j - 1] + 1]) <= t[i])
				++hd[j - 1];
			int tr = q[j - 1][hd[j - 1]];
			dp[j][i] = (dp[j - 1][tr] + t[i] * (i - tr) - S[i] + S[tr]);
			while(hd[j] < tl[j] && Slope(j,q[j][tl[j]],i) < Slope(j,q[j][tl[j] - 1],q[j][tl[j]]))
				--tl[j];
			q[j][++tl[j]] = i;
		}
	}
	cout << dp[p][m];
	return 0;
}

0x52 luogu P4360 [CEOI2004] 锯木厂选址

code:

#include<iostream>
#include<cstring>
using namespace std;
typedef long long LL;
const int N=200005;
LL n, d[N], sumD[N], w[N], sumW[N], sumDW[N], q[N];
LL f[N][5];
double long slope(int i, int j, int k) {
	return (f[i][k]+sumDW[i]-f[j][k]-sumDW[j])*1.0/(sumW[i]-sumW[j]);
}
int main() {
	cin>>n;
	for(int i=1; i<=n; i++) {
		cin>>w[i]>>d[i];
		sumD[i+1] = sumD[i]+d[i];
		sumW[i] = sumW[i-1]+w[i];
		sumDW[i] = sumDW[i-1]+sumD[i]*w[i];
	}
	memset(f, 0x3f, sizeof(f));
	f[0][0] = 0;
	for(int k=1; k<=3; k++) {
		int hh=1, tt=1;
		for(int i=1; i<=n+1; i++) {
			while(hh<tt && slope(q[hh+1], q[hh], k-1)<sumD[i]) hh++;
			int j = q[hh];
			f[i][k] = f[j][k-1]+sumD[i]*(sumW[i-1]-sumW[j]) - (sumDW[i-1]-sumDW[j]);
			while(hh<tt && slope(q[tt], q[tt-1], k-1)>=slope(i, q[tt-1], k-1)) tt--;
			q[++tt] = i;
		}
	}
	cout<<f[n+1][3];
	return 0 ;
}

0x53 luogu P2120 [ZJOI2007] 仓库建设

code:

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;

const int N=1e6+10;

int n,q[N],head,tail;
long long x[N],p[N],c[N],f[N],sp[N],s[N];

long long X( int num ) {
	return sp[num];
}
long long Y( int num ) {
	return f[num]+s[num];
}
long double slope( int n1,int n2 ) {
	return (long double)(Y(n2)-Y(n1))/(X(n2)-X(n1));
}

int main() {
	scanf( "%d",&n );
	for ( int i=1; i<=n; i++ )
		scanf( "%lld%lld%lld",&x[i],&p[i],&c[i] );
	sp[0]=s[0]=0;
	for ( int i=1; i<=n; i++ )
		sp[i]=sp[i-1]+p[i],s[i]=s[i-1]+p[i]*x[i];

	head=tail=1;
	q[1]=0;
	for ( int i=1; i<=n; i++ ) {
		while ( head<tail && slope(q[head],q[head+1])<=(long double)x[i] ) 
            head++;
		f[i]=f[q[head]]+x[i]*(sp[i]-sp[q[head]])-(s[i]-s[q[head]])+c[i];				
		while ( head<tail && slope(q[tail-1],q[tail])>=slope(q[tail],i) ) tail--;
	    	tail++;
		q[tail]=i;
	}
	
	long long haq=0x3f3f3f3f3f3f;
	for(int i=n;i>=1;i--){
		haq=min(haq,f[i]);
		if(p[i])break;
	}

	
	printf( "%lld",haq );
}

0x54 luogu P3195 [HNOI2008]玩具装箱

code:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std ;

const long long MAXN = 5e4+10 ;

long long n ,l ;
long long sum[MAXN] ;
long long q[MAXN] ;
long long f[MAXN] ;
long long head = 1 ;
long long tail = 1 ;//只有一个点时不能取斜率

inline long long x(long long a){
	return sum[a] ;
}

inline long long y(long long a){
	return f[a] + (sum[a] + l) * (sum[a] + l) ;//带入公式
}

inline long double slope(long long i , long long j){//计算斜率
	return (long double)(y(j) - y(i)) / (x(j) - x(i)) ;
}

int main(){
	scanf("%d%d" , &n , &l) ;
	l += 1 ;
	
	for(long long i = 1;i <= n;++i){
		scanf("%lld" , sum+i) ;
		sum[i] += sum[i-1] + 1 ;
	}
	
	for(long long i = 1;i <= n;++i){
		while(head < tail && slope(q[head] , q[head+1]) <= 2 * sum[i])
			head++ ;
		
		f[i] = f[q[head]] + (sum[i] - sum[q[head]] - l) * (sum[i] - sum[q[head]] - l) ;	
		
		while(head < tail && slope(q[tail-1] , q[tail]) >= slope(q[tail-1] , i))
			tail-- ;
				
		q[++tail] = i ;
	}
	
	printf("%lld" , f[n]) ;
	return 0 ;
}

0x55 luogu P3628 [APIO2010] 特别行动队

code:

#include<iostream>
#include<cstdio>
using namespace std ;

const int MAXN = 1e6+10 ;

long long n ;
long long a ,bb ,c ;
long long sum[MAXN] ;
long long f[MAXN] ;
long long q[MAXN] ;
long long head = 0 ;
long long tail = 0 ;

inline long long k(long long i){
    return 2 * a * sum[i] ;
}

inline long long y(long long i){
    return f[i] + a * sum[i] * sum[i] - bb * sum[i] ;
}

inline long long b(long long i){
    return f[i] - a * sum[i] * sum[i] - bb * sum[i] - c ;
}

inline long long x(long long i){
    return sum[i] ;
}

inline double slope(long long i , long long j){
    return 1.0 * (y(i) - y(j)) / (x(i) - x(j)) ;
}

int main(){
    scanf("%lld%lld%lld%lld" , &n , &a , &bb , &c) ;
    for(long long i = 1;i <= n;++i){
        scanf("%lld" , sum+i) ;
        sum[i] += sum[i-1] ;
    }
    
    for(long long i = 1;i <= n;++i){
        while(head < tail && slope(q[head] , q[head+1]) > k(i))
            head++ ;
        
        f[i] = -(k(i) * x(q[head]) - y(q[head]) - a * sum[i] * sum[i] - bb * sum[i] - c) ;
        
        while(head < tail && slope(q[tail-1] , q[tail]) <= slope(q[tail] , i))
            tail-- ;
            
        q[++tail] = i ;
    }
    
    printf("%lld" , f[n]) ;
    return 0 ;
}

0x56 luogu P3648 [APIO2014] 序列分割

code:

#include<iostream>
#include <cstdio>
#include <cstring>

const int MAXN = 1e5+5 ;
const int MAXM = 205 ;

int n ,k ,a[MAXN] ,q[MAXN] ,pre[MAXN][MAXM] ;
long long s[MAXN] ,f[MAXN] ,g[MAXN] ;

inline double slope(int i,int j) {
	if(s[i]==s[j]) return -1e18;
	return 1.0*((g[i]-s[i]*s[i]) - (g[j]-s[j]*s[j])) / (s[j]-s[i]) ;
}

int main() {
	scanf("%d%d" , &n , &k) ;
	for(int i=1; i<=n; ++i) 
		scanf("%d" , s+i) , s[i] += s[i-1] ;
		
	for(int j = 1;j <= k;++j){
		int head = 1 ,tail = 0 ;
		q[++tail] = 0 ;
		
		for(int i = 1;i <= n;++i){
			while(head < tail && slope(q[head] , q[head+1]) <= s[i]) 
				++head ;
				
			f[i] = g[q[head]] + s[q[head]] * (s[i]-s[q[head]]) ;
			pre[i][j] = q[head] ;
			
			while(head < tail && slope(q[tail-1] , q[tail]) >= slope(q[tail] , i)) 
				--tail ;
			q[++tail] = i ;
		}
		memcpy(g,f,sizeof(f));
	}
	printf("%lld\n" , f[n]) ;
	for(int x = n ,i = k;i >= 1;--i) 
		x = pre[x][i] , printf("%d " , x) ;
	return 0;
}

0x60 链接分享

巨佬%%%%
辰星凌的blog

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值