【动态规划】5 从一次函数出发推导斜率优化dp
背景
基于例题《任务安排》逐步推导进行斜率优化。
引入
例题:P2365 任务安排
考虑动态规划。使用 d p i , j dp_{i,j} dpi,j 表示前 i i i 个任务分了 j j j 段的最小费用。
显然,有 d p i , j = min k = 1 i − 1 ( d p i , j , d p k , j − 1 + ( t o t i − t o t k ) ) ∗ ( s u m [ i ] + s ∗ j ) ) dp_{i,j} = \min_{k=1}^{i-1} (dp_{i,j},dp_{k,j-1} + (tot_i-tot_k))*(sum[i]+s*j)) dpi,j=mink=1i−1(dpi,j,dpk,j−1+(toti−totk))∗(sum[i]+s∗j)) 。
- s u m i sum_i sumi 表示 c i c_i ci 的前缀和。
- t o t i tot_i toti 表示 t i t_i ti 的前缀和。
前缀和优化后,时间复杂度 O ( n 3 ) O(n^3) O(n3),得到 60pts.
代码
#include <bits/stdc++.h>
using namespace std;
int n,s,ans,t[5005],c[5005],dp[5005][5005],sum[5005],tot[5005];
int main()
{cin >> n >> s;for (int i=1;i<=n;i++){cin >> t[i] >> c[i];sum[i] = sum[i-1] + t[i];tot[i] = tot[i-1] + c[i];}memset(dp,0x3f,sizeof(dp));ans = 0x3f3f3f3f;dp[0][0] = 0;for (int i=1;i<=n;i++){for (int j=1;j<=i;j++){for (int k=0;k<i;k++){dp[i][j] = min(dp[i][j],dp[k][j-1] + (tot[i]-tot[k])*(sum[i]+s*j));} } }for (int i=1;i<=n;i++){ans = min(ans,dp[n][i]);}cout<<ans;return 0;
}
如何进一步优化呢?
我们发现,可以把有关 s s s 的计算在前面完成。也就是 费用提前计算 ,就不需要枚举分的段数了。
得到状态转移方程 d p i = min ( d p i , d p j + s u m i ∗ t o t i − s u m i ∗ t o t j + t o t n ∗ s − t o t j ∗ s ) dp_i = \min(dp_i,dp_j + sum_i*tot_i-sum_i*tot_j + tot_n*s-tot_j*s) dpi=min(dpi,dpj+sumi∗toti−sumi∗totj+totn∗s−totj∗s)
代码
#include <bits/stdc++.h>
using namespace std;
long long n,s,ans,t[5005],c[5005],dp[5005],sum[5005],tot[5005];
int main()
{cin >> n >> s;for (int i=1;i<=n;i++){cin >> t[i] >> c[i];sum[i] = sum[i-1] + t[i];tot[i] = tot[i-1] + c[i];dp[i] = 1e18;}ans = 1e18;dp[0] = 0;for (int i=1;i<=n;i++){for (int j=0;j<i;j++){dp[i] = min(dp[i],dp[j] + sum[i]*(tot[i]-tot[j]) + (tot[n]-tot[j])*s);} }cout<<dp[n];return 0;
}
正文
状态转移方程 d p i = min ( d p i , d p j + s u m i ∗ t o t i − s u m i ∗ t o t j + t o t n ∗ s − t o t j ∗ s ) dp_i = \min(dp_i,dp_j + sum_i*tot_i-sum_i*tot_j + tot_n*s-tot_j*s) dpi=min(dpi,dpj+sumi∗toti−sumi∗totj+totn∗s−totj∗s)
把与 i , j i,j i,j 有关的各单独放在一起,得到 d p i = min ( d p i , d p j + s u m i ∗ t o t i + t o t n ∗ s − t o t j ∗ ( s u m i + s ) ) dp_i = \min(dp_i,dp_j + sum_i*tot_i + tot_n*s - tot_j*(sum_i+s)) dpi=min(dpi,dpj+sumi∗toti+totn∗s−totj∗(sumi+s))
去掉最小值,得到 d p i = d p j + s u m i ∗ t o t i + t o t n ∗ s − t o t j ∗ ( s u m i + s ) dp_i = dp_j + sum_i*tot_i + tot_n*s - tot_j*(sum_i+s) dpi=dpj+sumi∗toti+totn∗s−totj∗(sumi+s)
移项,得到 d p j = t o t j ∗ ( s u m i + s ) + d p i − s u m i ∗ t o t i − t o t n ∗ s dp_j = tot_j*(sum_i+s) + dp_i - sum_i*tot_i - tot_n*s dpj=totj∗(sumi+s)+dpi−sumi∗toti−totn∗s
在 t o t j tot_j totj 为横坐标, d p j dp_j dpj 为纵坐标的平面直角坐标系中,
这是一条 y = ( s + s u m i ) ∗ x + d p i − s u m i ∗ t o t i − t o t n ∗ s y = (s+sum_i) * x + dp_i - sum_i * tot_i - tot_n * s y=(s+sumi)∗x+dpi−sumi∗toti−totn∗s 的直线。
写成 y = k x + b y = kx+b y=kx+b 的形式, k = s + s u m i k = s+sum_i k=s+sumi , b = d p i − s u m i ∗ t o t i − t o t n ∗ s b = dp_i-sum_i*tot_i-tot_n*s b=dpi−sumi∗toti−totn∗s.
由于 k k k 是定值,所求的 d p i dp_i dpi 存在于 b b b 中,所以我们只需要找到最小的 b b b 即可。
如何寻找最小的 b b b ?
发现有一些点会出现在这条直线上,我们把这样的点称为 决策点,即 ( t o t j , d p j ) (tot_j,dp_j) (totj,dpj)。
对于这些决策点,由于 k k k 是定值,所以有且只有一条 k = s + s u m i k=s+sum_i k=s+sumi 的直线经过一个决策点,这些决策点一共会产生不超过 j j j 条直线。
对于已知的一个决策点 ( t o t j , d p j ) (tot_j,dp_j) (totj,dpj),我们把它们带入到一次函数表达式里去,就能解出一个 b b b ,枚举 j j j 得到最小的 b b b 即可。
但这种方法过于朴素,时间复杂度不变。考虑优化。
由于我们是从决策点出发,推导 b b b 的值。则说明决策点坐标(或者说 j j j )与 b b b 之间存在线性关系。考虑决策点坐标之间的关系来优化。
对于三个决策点 ( t o t j 1 , d p j 1 ) , ( t o t j 2 , d p j 2 ) , ( t o t j 3 , d p j 3 ) (tot_{j_1},dp_{j_1}),(tot_{j_2},dp_{j_2}),(tot_{j_3},dp_{j_3}) (totj1,dpj1),(totj2,dpj2),(totj3,dpj3) (我们设这三点 j 1 < j 2 < j 3 j_1 < j_2 < j_3 j1<j2<j3 ,由于 t , c > 0 t,c > 0 t,c>0 ,所以这三点的横坐标依次递增,即 t o t j 1 < t o t j 2 < t o t j 3 tot_{j_1} < tot_{j_2} < tot_{j_3} totj1<totj2<totj3)来说,当这三个决策点有且仅有取 ( t o t j 2 , d p j 2 ) (tot_{j_2},dp_{j_2}) (totj2,dpj2) 时, b b b 有最小值,那么这三点所构成的直线不会两两重合,并分为两种情况:
情况 1 ( j 2 j_2 j2 在 j 1 j_1 j1 与 j 3 j_3 j3 的连线上方)
当这三点构成一个向上凸出的形状,即 上凸 。显然此时 j 2 j_2 j2 一定不会使得 b b b 取最小值,如下图所示。
情况 2 ( j 2 j_2 j2 在 j 1 j_1 j1 与 j 3 j_3 j3 的连线下方)
当这三点构成一个向下凸出的形状,即 下凸 。显然此时 j 2 j_2 j2 可能使得 b b b 取最小值,如下图所示。
发现只有 下凸 的情况 ( j 2 j_2 j2 在 j 1 j_1 j1 与 j 3 j_3 j3 的连线下方) 才可能使 j 2 j_2 j2 取到最小的 b b b 。
则有 d p j 2 − d p j 1 t o t j 2 − t o t j 1 < d p j 3 − d p j 2 t o t j 3 − t o t j 2 \frac{dp_{j_2}-dp_{j_1}}{tot_{j_2}-tot_{j_1}} < \frac{dp_{j_3}-dp_{j_2}}{tot_{j_3}-tot_{j_2}} totj2−totj1dpj2−dpj1<totj3−totj2dpj3−dpj2 。
即直线 j 1 → j 2 j_1 \to j_2 j1→j2 的 k k k 小于 j 2 → j 3 j_2 \to j_3 j2→j3 直线的 k k k ,本质上是这两条直线的斜率关系。
因此,我们需要维护 相邻两点间直线的 k k k (斜率) ,并当它们 单调递增 时, j 2 j_2 j2 所得到的 b b b 就可能是最小值。
那么什么时候 j 2 j_2 j2 所取的 b b b 就一定是最小值呢?
我们发现,当一段单调递增的 k k k 满足一个点的左边的 k ’ k’ k’ 都小于 k k k ,右边的 k ’ k’ k’ 都大于 k k k 时,这个点就是使 b b b 最小的点。
如果我们只维护 相邻两点间连线斜率大于等于 k k k 的 k ′ k' k′ (斜率),那么在这个单调递增的序列中最小值就能使 b b b 最小。
这不就是单调队列的思路吗?
总结一下:
- 我们用单调队列维护相邻两点间直线的 k k k ,使其单调递增。
- 在单调队列里放的是 k k k 单调递增的点的编号。
- 最终答案是单调队列的队头坐标代入 d p i = d p j + s u m i ∗ t o t i + t o t n ∗ s − t o t j ∗ ( s u m i + s ) dp_i = dp_j + sum_i*tot_i + tot_n*s - tot_j*(sum_i+s) dpi=dpj+sumi∗toti+totn∗s−totj∗(sumi+s).
- 为了维护单调性,我们需要从左侧队头开始删除。即判断队头斜率 d p q h e a d + 1 − d p q h e a d t o t q h e a d + 1 − t o t q h e a d ≤ s + s u m i \frac{dp_{q_{head+1}}-dp_{q_{head}}}{tot_{q_{head+1}}-tot_{q_{head}}} \leq s+sum_i totqhead+1−totqheaddpqhead+1−dpqhead≤s+sumi 时,把队头出队即可。 为了避免精度问题,且 t o t tot tot 有单调递增性,那么我们不妨判断 d p q h e a d + 1 − d p q h e a d ≤ ( s + s u m i ) ∗ ( t o t q h e a d + 1 − t o t q h e a d ) {dp_{q_{head+1}}-dp_{q_{head}}} \leq (s+sum_i) * ({tot_{q_{head+1}}-tot_{q_{head}}}) dpqhead+1−dpqhead≤(s+sumi)∗(totqhead+1−totqhead).
- 添加时,如果 q i q_i qi 不能与前面的点满足单调性,那么直接把前面的点不断出队,直到满足单调性为止。即当 d p i − d p q t a i l t o t i − t o t q t a i l ≤ d p q t a i l − d p q t a i l − 1 t o t q t a i l − t o t q t a i l − 1 \frac{dp_{i}-dp_{q_{tail}}}{tot_{i}-tot_{q_{tail}}} \leq \frac{dp_{q_{tail}}-dp_{q_{tail-1}}}{tot_{q_{tail}}-tot_{q_{tail-1}}} toti−totqtaildpi−dpqtail≤totqtail−totqtail−1dpqtail−dpqtail−1 时不断出队即可。同样避免精度问题,判断 ( d p i − d p q t a i l ) ∗ ( t o t q t a i l − t o t q t a i l − 1 ) ≤ ( d p q t a i l − d p q t a i l − 1 ) ∗ ( t o t i − t o t q t a i l ) ({dp_{i}-dp_{q_{tail}}}) * ({tot_{q_{tail}}-tot_{q_{tail-1}}}) \leq ({dp_{q_{tail}}-dp_{q_{tail-1}}})*({tot_{i}-tot_{q_{tail}}}) (dpi−dpqtail)∗(totqtail−totqtail−1)≤(dpqtail−dpqtail−1)∗(toti−totqtail) 即可。
时间复杂度 O ( n ) O(n) O(n) .
#include <bits/stdc++.h>
using namespace std;
const int N = 300005;
long long n,s,ans,t[N],c[N],dp[N],sum[N],tot[N];
long long q[N],head=1,tail=1;
int main()
{cin >> n >> s;for (int i=1;i<=n;i++){cin >> t[i] >> c[i];sum[i] = sum[i-1] + t[i];tot[i] = tot[i-1] + c[i];dp[i] = 1e18;}ans = 1e18;dp[0] = 0;for (int i=1;i<=n;i++){while (head < tail && dp[q[head+1]]-dp[q[head]] <= (s+sum[i])*(tot[q[head+1]]-tot[q[head]])) head++;dp[i] = dp[q[head]] + sum[i]*tot[i] + tot[n]*s - tot[q[head]]*(sum[i]+s);while (head < tail && (dp[i]-dp[q[tail]])*(tot[q[tail]]-tot[q[tail-1]]) <= (dp[q[tail]]-dp[q[tail-1]])*(tot[i]-tot[q[tail]])) tail--;q[++tail] = i;}cout<<dp[n];return 0;
}
为什么单调队列初始
head = 1,tail = 1
,而不能写作head = 1,tail = 0
?
考虑到还有dp[0] = 0
,要么把tail
设为head
,要么把tail
设为head-1
再在队列里加入dp[0]
。
后记
斜率优化看起来好像的确比想象中抽象很多,希望对大家理解斜率优化有所帮助!