斜率优化
之前说过好几次要写斜率优化写过都咕咕掉了qwq
引入
相信各位都知道一种叫做线性规划的题目,举个例子:
求 z = x + y z = x + y z=x+y 的最大值,其中 x x x 和 y y y 满足:
{ x + y ≥ 0 2 x − y ≥ 0 x ≤ 1 \begin{cases} x + y \geq 0 \\ 2x - y \geq 0 \\ x \leq 1 \end{cases} ⎩⎪⎨⎪⎧x+y≥02x−y≥0x≤1
解决这个问题,我们就需要画出平面直角坐标系,然后在其中找到满足上述条件的点对,就像这样:
显然由 A , B , C A, B, C A,B,C 三个点所构成的三角形的范围就是满足上述不等式组的解集。然后我们再看 z = x + y z = x +y z=x+y 的最值,我们稍微把这个式子变一下形:
y = − x + z y = -x + z y=−x+z
这样一来,这个式子就变成了平面直角坐标系中的一条直线,又因为有上面的约束条件,所以这条线必定经过这个三角形内的一点,就像这样:
现在要求 z z z 的最大值结果就很显然了, z z z 也就是那条黑色直线的纵截距,要让它最大,那这条黑线肯定就是过点 A ( 1 , 2 ) A(1, 2) A(1,2) 的。所以 z m a x = 3 z_{max} = 3 zmax=3
在这一类的线性规划问题中,我们发现,不论我们要求的 z z z 值所形成的直线的斜率是多少,是求它的最大值还是最小值,显然我们画出的范围中最外围的点一定是比里面的更优的。也就是说我们只用考虑经过 “凸包” 上的点的贡献就可以了。
回到正题
这样的思想也可以运用到动态规划的优化上面,就比如说现在我们的 d p dp dp 方程可以写成 y = k x + b y = kx + b y=kx+b 的样式,其中 b b b 就是我们要求的 f i f_i fi(而且我们要最大化或者最小化 f i f_i fi),然后现在有很多个 ( x , y ) (x, y) (x,y) 的决策点。我们就可以只用考虑凸包上的决策点对答案的贡献,因为凸包上的点一定比凸包里面的点更优。
画个图以便更好地理解:
这张图里面,我们就只用考虑那些红点的贡献就可以了。
举个例题
注意这道题正常的斜率优化只能过 60 p t s 60pts 60pts。
题目描述
n n n 个任务排成一个序列在一台机器上等待完成(顺序不得改变),这 n n n 个任务被分成若干批,每批包含相邻的若干任务。
从零时刻开始,这些任务被分批加工,第 i i i 个任务单独完成所需的时间为 t i t_i ti。在每批任务开始前,机器需要启动时间 s s s,而完成这批任务所需的时间是各个任务需要时间的总和(同一批任务将在同一时刻完成)。
每个任务的费用是它的完成时刻乘以一个费用系数 c i c_i ci。请确定一个分组方案,使得总费用最小。
算法分析
设 f i , j f_{i, j} fi,j 表示前 i i i 个任务被划分成 j j j 批的最小费用,并且我们做 c i c_i ci 和 t i t_i ti 的前缀和分别表示为 C i C_i Ci 和 T i T_i Ti,即:
C i = ∑ k = 1 i c k T i = ∑ k = 1 i t k C_i = \sum_{k=1}^i c_k \qquad T_i = \sum_{k = 1}^it_k Ci=k=1∑ickTi=k=1∑itk
然后我们就能得到:
f i , j = min k = 0 i − 1 { f k , j − 1 + ( s × j + T i ) ( C i − C k ) } f_{i, j} = \min_{k = 0}^{i-1}\{ f_{k, j - 1} + (s \times j + T_i) (C_i - C_k) \} fi,j=k=0mini−1{fk,j−1+(s×j+Ti)(Ci−Ck)}
也就是前 k k k 个被划分成了 j − 1 j - 1 j−1 批,第 j j j 批就是从 k k k 到 i i i。那么结束的时刻就是 ( s × j + T i ) (s \times j + T_i) (s×j+Ti),然后第 j j j 批的贡献就是要再乘上一个 ( C i − C k ) (C_i - C_k) (Ci−Ck)。
但是我们一看,这个式子先要枚举 i , j i, j i,j 还要枚举 k k k 复杂度直接就 O ( n 3 ) O(n^3) O(n3) 起,所以我们考虑优化这个式子。
第一个优化还不是斜率优化。
上面的式子中,我们之所以会假设分成 j j j 批是因为我们需要知道总共的开机时间,也就是 s × j s \times j s×j 的那一项。如果我们删掉 j j j 这一维那就不太方便知道到底启动了多少次,但是我们知道机器因为执行这一批任务而花费的启动时间会累加到之后所有的任务上,所以我们可以这样:
设 f i f_i fi 表示把前 i i i 个任务分成若干批完成所需要的最小费用,于是我们就有:
f i = min j = 0 i − 1 { f j + T i ( C i − C j ) + s ( C n − C j ) } f_i = \min_{j = 0}^{i - 1}\{ f_j + T_i(C_i - C_j) + s(C_n - C_j) \} fi=j=0mini−1{fj+Ti(Ci−Cj)+s(Cn−Cj)}
上式中, j + 1 ∼ i j + 1 \sim i j+1∼i 的所有任务在同一批内执行, T i T_i Ti 就是忽略了启动时间的完成时间,然后后面我们加上的一坨东西就是这次启动时间对后面所有任务时间的贡献。这样做的时间复杂度就降到 O ( n 2 ) O(n^2) O(n2) 了。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
#define MAXN 300300
inline int read(){
int x = 0; char c = getchar();
while(c < '0' or c > '9') c = getchar();
while('0' <= c and c <= '9'){
x = x * 10 + c - '0'; c = getchar();
}
return x;
}
int t[MAXN] = { 0 };
int c[MAXN] = { 0 };
int T[MAXN] = { 0 };
int C[MAXN] = { 0 };
int f[MAXN] = { 0 };
int n = 0; int s = 0;
signed main(){
n = in; s = in;
for(int i = 1; i <= n; i++) t[i] = in, c[i] = in;
for(int i = 1; i <= n; i++) T[i] = T[i - 1] + t[i], C[i] = C[i - 1] + c[i];
memset(f, 0x3f, sizeof(f)); f[0] = 0;
for(int i = 1; i <= n; i++)
for(int j = 0; j < i; j++)
f[i] = min(f[i], f[j] + T[i] * (C[i] - C[j]) + s * (C[n] - C[j]));
cout << f[n] << '\n';
return 0;
}
这样就能得到 20 p t s 20pts 20pts 的高分。
现在终于来到斜率优化了,我们把式子再变一下:
f i = f j + T i ( C i − C j ) + s ( C n − C j ) → f i = − ( s + T i ) C j + T i C i + s C n + f j → f j = ( s + T i ) C j + f i − T i C i − s C n \begin{aligned} & f_i = f_j + T_i(C_i - C_j) + s(C_n - C_j) \\ \rightarrow & f_i = -(s + T_i)C_j + T_iC_i + sC_n + f_j\\ \rightarrow & f_j = (s+T_i)C_j + f_i - T_iC_i - sC_n \end{aligned} →→fi=fj+Ti(Ci−Cj)+s(Cn−Cj)fi=−(s+Ti)Cj+TiCi+sCn+fjfj=(s+Ti)Cj+fi−TiCi−sCn
这里,我们令 y = f j , k = s + T i , x = C j , b = f i − T i C i − s C n y = f_j, k = s+T_i, x = C_j, b = f_i - T_iC_i - sC_n y=fj,k=s+Ti,x=Cj,b=fi−TiCi−sCn,那么这个式子就被写成了 y = k x + b y = kx + b y=kx+b 的形式了。
现在决策候选的集合就被转变成了坐标系中的一个点集,每一个决策 j j j 都对应着一个点 ( x , y ) (x, y) (x,y) 也就是 ( C j , f j ) (C_j, f_j) (Cj,fj)。然后每个带求解的状态 f i f_i fi 都对应着一条直线的截距,直线的斜率是一个定值 k = s + T i k = s +T_i k=s+Ti。
显然现在我们就能用我们上面引入中说道的方法来解决这个问题了。具体的,我们现在是要最小化截距,所以应该就是只需要维护一个下凸壳就可以了(显然上面的点没有下面的点优)。
现在我们就要考虑如何维护这个下凸壳,我们发现,对于一个下凸壳来说,这上面相邻两点之间的斜率是单调递增的,根据这个性质,我们具体这样维护:
在这道题里面,我们每次加入的新的决策点 ( C i , f i ) (C_i, f_i) (Ci,fi) 显然横坐标是单调递增的。也就是说新加进来的点肯定在已经维护好的下凸壳的所有点的右边。另外的,又因为 T i T_i Ti 也是单调递增的,所以每次的决策的斜率 k = s + T i k = s + T_i k=s+Ti 也就是单调递增的。如果我们只保留下凸壳上斜率大于 k = s + T i k = s +T_i k=s+Ti 的部分,那么最左边的端点也就是取到答案的决策点。
综上所述,我们可以建立一个单调队列 q q q,维护这个下凸壳,队列中保存几个决策变量,他们对应下凸壳上的点,并且满足横坐标是递增的。相邻两点的斜率也是递增的。对于每个状态变量:
- 对于队首的两个决策变量,如果满足这两个点的斜率 k ′ = f q [ l + 1 ] − f q [ l ] C q [ l + 1 ] − C q [ l ] ≤ k = s + T i k' = \frac{f_{q[l + 1] } - f_{q[l]}}{C_{q[l + 1]} - C_{q[l]}} \leq k = s + T_i k′=Cq[l+1]−Cq[l]fq[l+1]−fq[l]≤k=s+Ti 的话,那就弹出队首,然后继续检查新的队首。
- 取出队首 h = q [ l ] h = q[l] h=q[l],这个就是最优的决策点,然后进行状态转移,算出 f i f_i fi
- 把新的决策点 ( C i , f j ) (C_i, f_j) (Ci,fj) 放入队尾,再放入之前要检查一下是否满足斜率单调递增,如果不满足则队尾出队,继续检查新的队尾。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
#define MAXN 300300
inline int read(){
int x = 0; int f = 1; char c = getchar();
while(c < '0' or c > '9'){
if(c == '-') f = -1; c = getchar();
}
while('0' <= c and c <= '9'){
x = x * 10 + c - '0'; c = getchar();
}
return f * x;
}
int t[MAXN] = { 0 };
int c[MAXN] = { 0 };
int T[MAXN] = { 0 };
int C[MAXN] = { 0 };
int f[MAXN] = { 0 };
int n = 0; int s = 0;
int q[MAXN] = { 0 };
void dp(){
int l = 1, r = 1; q[1] = 0;
for(int i = 1; i <= n; i++){
while(l < r and (f[q[l + 1]] - f[q[l]]) <= (s + T[i]) * (C[q[l + 1]] - C[q[l]]) ) l++;
f[i] = f[q[l]] - (s + T[i]) * C[q[l]] + T[i] * C[i] + s * C[n];
while(l < r and (f[q[r]] - f[q[r - 1]]) * (C[i] - C[q[r]]) >= (f[i] - f[q[r]]) * (C[q[r]] - C[q[r - 1]])) r--;
q[++r] = i;
}
}
signed main(){
n = in; s = in;
for(int i = 1; i <= n; i++) t[i] = in, c[i] = in;
for(int i = 1; i <= n; i++) T[i] = T[i - 1] + t[i], C[i] = C[i - 1] + c[i];
memset(f, 0x3f, sizeof(f)); f[0] = 0; dp();
cout << f[n] << '\n';
return 0;
}
这样我们就能愉快的拿到 60 p t s 60 pts 60pts 了。
剩下的数据就不满足 t i > 0 t_i > 0 ti>0 就不能用我们上面的方法来维护凸包了,要写一个平衡树维护凸包,这就不是我们要讨论的范围了,所以就这样吧。
完结撒花!!!