【DP】斜率优化学习笔记

· · 个人记录

前言

斜率优化:看起来很难,学起来异常水。

十分套路,但也十分灵活的一种优化。

一、套路

斜率优化一般用来解决一下这种 DP 式:

f_i=\min\{f_j+a_ib_j+c_i+d_j\}

f_i=\max\{f_j+a_ib_j+c_i+d_j\}

其中 a_ib_j 这一项既有 i 又有 j,因此不能单调队列优化。

那么我们设 j1,j2 满足 j1<j2j2 优于 j1,移项后可以得到:

\dfrac{X(j2)-X(j1)}{Y(j2)-Y(j1)}<k

\dfrac{X(j2)-X(j1)}{Y(j2)-Y(j1)}>k

我们先来讨论情况 1

如果存在 2 个数 j1j2 满足情况 1j1<j2 的话,那么很显然 j2 优于 j1,也就是说,如果 (X(j1),Y(j1))(X(j2),Y(j2)) 构成的直线的斜率 <k,那么 j2 优于 j1,否则 j1 优于 j2

如图,平面上有点 A,B,C,其中 k1AB 所在直线的斜率,k2BC 所在直线的斜率,满足 k2<k1

根据上述结论,得:

k<k2<k1 时,明显 A 优于 B 优于 C

k2<k<k1 时,明显 A,C 均比 B 优。

k2<k1<k 时,明显 C 优于 B 优于 A

我们发现,无论如何,B 均不是最优,于是将 B 丢出去,那么就得到了这样一个图像:

我们发现这就是一个维护下凸壳的过程,于是用单调队列维护下凸壳即可。

那么对于第一种情况,维护下凸壳,第二种情况同理,维护上凸壳即可。

PS:只有当斜率 k 具有单调性的时候能这么做,否则需要在凸壳上二分。

二、例题

[ZJOI2007]仓库建设

f_i 表示到 i 的最小代价,显然可以得出:

f_i=\min\{f_j+\sum_{k=j+1}^{i}p_kx_i-\sum_{k=j+1}^ip_kx_k+c_i\}

sump_i=\sum_{j=1}^ip_jsum_i=\sum_{j=1}^ip_jx_j,化简得:

f_i=\min\{f_j+sump_ix_i-sump_jx_i-sum_i+sum_j+c_i\}

我们按照套路,设 j1,j2 满足 j1<j2j2 优于 j1,得:

f_{j2}+sump_ix_i-sump_{j2}x_i-sum_i+sum_{j2}+c_i<f_{j1}+sump_ix_i-sump_{j1}x_i-sum_i+sum_{j1}+c_i f_{j2}-sump_{j2}x_i+sum_{j2}<f_{j1}-sump_{j1}x_i+sum_{j1} (f_{j2}+sum_{j2})-(f_{j1}+sum_{j1})<(sump_{j2}-sump_{j1})x_i \dfrac{(f_{j2}+sum_{j2})-(f_{j1}+sum_{j1})}{sump_{j2}-sump_{j1}}<x_i

X(i)=f_i+sum_iY(i)=sump_i,那么就转化成了上面的情况 1,于是维护下凸壳即可。

Code: ```cpp #include <cstdio> #define int long long using namespace std ; const int MAXN = 1e6 + 10 ; int n , L , f[MAXN] , sum[MAXN] , sump[MAXN] , c[MAXN] , x[MAXN] , p[MAXN] ; int q[MAXN] , head = 1 , tail = 1 ; double slope (int j1 , int j2) { return (f[j2] + sum[j2] - f[j1] - sum[j1]) * 1.0 / (sump[j2] - sump[j1]) ; } signed main () { scanf ("%lld" , &n) ; for (int i = 1 ; i <= n ; i++) { scanf ("%lld %lld %lld" , &x[i] , &p[i] , &c[i]) ; sump[i] = sump[i - 1] + p[i] ; sum[i] = sum[i - 1] + p[i] * x[i] ; } for (int i = 1 ; i <= n ; i++) { while (head < tail && slope (q[head] , q[head + 1]) < x[i]) head++ ; int j = q[head] ; f[i] = f[j] + sump[i] * x[i] - sump[j] * x[i] - sum[i] + sum[j] + c[i] ; while (head < tail && slope (q[tail] , i) <= slope (q[tail - 1] , q[tail])) tail-- ; q[++tail] = i ; } printf ("%lld" , f[n]) ; return 0 ; } ``` #### [[APIO2010]特别行动队](https://www.luogu.com.cn/problem/P3628) 还是按照套路,设 $f_i$ 表示到 $i$ 时的答案,设 $sum_i=\sum_{j=1}^ix_j$,易得: $$f_i=\max\{f_j+a(sum_i-sum_j)^2+b(sum_i-sum_j)+c\}$$ 我们还是套路,设 $j1,j2$ 满足 $j1<j2$ 且 $j2$ 优于 $j1$,得: $$f_{j2}-2asum_isum_{j2}+asum_{j2}^2-bsum_{j2}>f_{j1}-2asum_isum_{j1}+asum_{j1}^2-bsum_{j1}$$ $$(f_{j2}+asum_{j2}^2-bsum_{j2})-(f_{j1}+asum_{j1}^2-bsum_{j1})>(sum_{j2}-sum_{j1})2asum_i$$ $$\dfrac{(f_{j2}+asum_{j2}^2-bsum_{j2})-(f_{j1}+asum_{j1}^2-bsum_{j1})}{sum_{j2}-sum_{j1}}>2asum_i$$ 我们设 $X(i)=f_i+asum_i^2-bsum_i$,$Y(i)=sum_i$,那么就转化成了上面的情况 $2$,维护上凸壳即可。 Code: ```cpp #include <cstdio> #define int long long using namespace std ; const int MAXN = 1e6 + 10 ; int n , a , b , c , f[MAXN] , sum[MAXN] ; int q[MAXN] , head = 1 , tail = 1 ; double slope (int j1 , int j2) { return ((f[j2] + a * sum[j2] * sum[j2] - b * sum[j2]) - (f[j1] + a * sum[j1] * sum[j1] - b * sum[j1])) * 1.0 / (sum[j2] - sum[j1]) ; } signed main () { scanf ("%lld %lld %lld %lld" , &n , &a , &b , &c) ; for (int i = 1 ; i <= n ; i++) { scanf ("%lld" , &sum[i]) ; sum[i] += sum[i - 1] ; } for (int i = 1 ; i <= n ; i++) { while (head < tail && slope (q[head] , q[head + 1]) > 2 * a * sum[i]) head++ ; int j = q[head] ; f[i] = f[j] + a * (sum[i] - sum[j]) * (sum[i] - sum[j]) + b * (sum[i] - sum[j]) + c ; while (head < tail && slope (q[tail - 1] , q[tail]) <= slope (q[tail] , i)) tail-- ; q[++tail] = i ; } printf ("%lld" , f[n]) ; return 0 ; } ```