斜率优化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,j−1]+(S∗j+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<=3∗105
这种算法很显然会超时。
这时候就要用到一种方式进行优化时间
这时候我们就要用到 斜率优化了
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]+S∗sumC[N]−S∗sumC[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]−S∗sumC[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[tail−1]]))(f[q[tail]]−f[q[tail−1]])
至此,我们终于引入了“斜率”与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