「学习笔记」斜率优化dp

2023-05-02,,

目录
算法
例题
任务安排
题意
思路
代码
[SDOI2012]任务安排
题意
思路
代码
任务安排 再改
题意
思路
练习题
[HNOI2008]玩具装箱
思路
代码
[APIO2010]特别行动队
思路
代码
[ZJOI2007]仓库建设
思路
代码
[USACO08MAR]Land Acquisition G
思路
代码

算法

把一些 dp 的转移方程拆一拆,移一移,能拆成 \(y=kx+b\) 的形式(其中 \(k,b\) 只与当前的 \(i\) 有关,\(x,y\) 只与 \(j\) 有关,\(b\) 包含 \(f_i\))。

然后我们用单调队列维护一些点形成的凸包,每次找一个最优的点。

详见例题任务安排。

例题

任务安排

题意

有 \(N\) 个任务,每个任务都有一个所需时间 \(T_i\) 和费用 \(C_i\)。

现在要求你把这些任务分成若干批,每批任务所需费用为机器启动时间 \(S\) 加上这批任务的总时间再乘上这批任务的总费用。

求最小费用。

\(1 \le n \le 3 \times 10^5\),\(1 \le s \le 2^8\),\(0\le T_i \le 2^8\),\(0 \le C_i \le 2^8\)。

思路

转移方程为:

\[f_i=\min_{1\le j\le i}\{f_j+SumT_i*(SumC_i-SumC_j)+S*(SumC_n-SumC_j)\}\\
\]

咋算的?

设 \(f_i\) 表示把前 \(i\) 个任务划分成了若干段的最小费用,那么我们要找到一个 \(j\),使 \(f_j\) 加上 \((j+1,i)\) 这些任务的费用的总费用最小。

其中,\(SumT_i*(SumC_i-SumC_j)\) 是这些任务执行完的总费用,\(S*(SumC_n-SumC_j)\) 是这个启动时间对之后的所用任务产生的影响

拆一下可得:

\[f_i=\min_{1\le j\le i}\{f_j+SumT_i*SumC_i-SumT_i*SumC_j+S*SumC_n-S*SumC_j\}\\
\]

原式去掉 \(\min\) 得:

\[f_j=SumT_i*SumC_j-SumT_i*SumC_i-S*SumC_n+S*SumC_j+f_i\\
\]

进一步变形为:

\[f_j=(SumT_i+S)*SumC_j-SumT_i*SumC_i-S*SumC_n+f_i\\
\]

此时令:

\[\begin{cases}
x=SumC_j\\
y=f_j\\
k=(SumT_i+S)\\
b=f_i-SumT_i*SumC_i-S*SumC_n\\
\end{cases}
\]

则原转移方程就变成了一个直线方程 \(y=kx+b\)。

我们要让 \(f_i\) 尽量小,就是让截距 \(b\) 尽量小.

那么画一个横坐标 \(SumC_j\),纵坐标 \(f_j\) 的坐标系。

我们把 \((SumC_j,f_j)\ (1\le j<i)\) 这些点都放到坐标系上。

然后我们把直线 \(y=kx+b\) 放上去。

再往上移移移移移,看能碰到那个点,那个点就是我们想要的使 \(f_i\) 最小的点。

可以发现,有可能被碰上的点在一个下凸包上。

那么我们只要用单调队列维护这个相邻两点之间斜率递增的下凸包就可以了。

每加入一个点,都要保证这些点的斜率是递增的,即 \(\dfrac{f_{tail}-f_{tail-1}}{SumC_{tail}-SumC_{tail-1}}\ge\dfrac{f_{i}-f_{tail}}{Sum_{i}-SumC_{tail}}\)。

对于这道题,\(SumT\) 是递增的,也就是说斜率是递增的。

那么我们每次可以把所有在队头的斜率比当前斜率小的点给弹出去,找 \(f_j\) 时直接找队头。这样可以大大减少时间。

时间复杂度是 \(O(n)\)。

一定不要 像我一样 在移项的时候推错式子!!!

代码

点击查看代码
namespace DP{
inline ll x(ll i){return sc[i];}
inline ll y(ll i){return f[i];}
inline ll k(ll i){return st[i]+s;}
inline ll b(ll i){return f[i]-st[i]*sc[i]-s*sc[n];}
void Dp(){
ll q[N]={0},hd=1,tl=1;
memset(q,0,sizeof(q));
_for(i,1,n){
#define h(o) q[hd+o]
#define t(o) q[tl-o]
while(hd<tl&&(y(h(1))-y(h(0)))<=k(i)*(x(h(1))-x(h(0))))
++hd;
ll j=h(0);
f[i]=y(j)-k(i)*x(j)-b(i);
f[i]=f[j]+st[i]*(sc[i]-sc[j])+s*(sc[n]-sc[j]);
while(hd<tl&&(x(t(0))-x(t(1)))*(y(i)-y(t(0)))<=(x(i)-x(t(0)))*(y(t(0))-y(t(1))))
--tl;
q[++tl]=i;
#undef h
#undef t
}
return;
}
}

[SDOI2012]任务安排

题意

题意和上一个任务安排一样。

\(1 \le n \le 3 \times 10^5\),\(1 \le s \le 2^8\),$ \left| T_i \right| \le 2^8$,\(0 \le C_i \le 2^8\)。

思路

和上一个任务安排相比,这里的 \(T_i\) 可以为负数。

也就是说,此时我们的斜率 \(SumT_i+S\) 不再具有单调性,那么我们就不能把队头的元素弹出去了,而是用二分找最优点。

时间复杂度是 \(O(nlogn)\)。

代码

点击查看代码
namespace DP{
ll q[N]={0},hd=1,tl=1;
inline ll x(ll i){return sc[i];}
inline ll y(ll i){return f[i];}
inline ll k(ll i){return st[i]+s;}
inline ll b(ll i){return f[i]-st[i]*sc[i]-s*sc[n];}
inline ll fd(ll i){
ll l=hd,r=tl;
while(l<r){
ll mid=(l+r)>>1;
if(y(q[mid+1])-y(q[mid])<=k(i)*(x(q[mid+1])-x(q[mid])))
l=mid+1;
else r=mid;
}
return l;
}
void Dp(){
memset(q,0,sizeof(q));
_for(i,1,n){
#define h(o) q[hd+o]
#define t(o) q[tl-o]
ll j=q[fd(i)];
f[i]=y(j)-k(i)*x(j)-b(i);
while(hd<tl&&(x(t(0))-x(t(1)))*(y(i)-y(t(0)))<=(x(i)-x(t(0)))*(y(t(0))-y(t(1))))
--tl;
q[++tl]=i;
#undef h
#undef t
}
return;
}
}

任务安排 再改

题意

题意和上一个任务安排一样。

\(1 \le n \le 3 \times 10^5\),\(1 \le s \le 2^8\),$ \left| T_i \right| , \left| C_i \right| \le 2^8$。

思路

要用到平衡树或 CDQ 分治维护凸包。

但是作者还没学,所以它咕咕咕了。

练习题

[HNOI2008]玩具装箱

思路

随便推一推:

\[f_i=\min_{1\le j<i}\{f_j+(i-j-1+Sum_i-Sum_j-L)^2\}
\]
\[f_i=\min_{1\le j<i}\{f_j+[(i+Sum_i-1-L)-(j+Sum_j)]^2\}
\]
\[f_i=\min_{1\le j<i}\{f_j+(i+Sum_i-1-L)^2-2(i+Sum_i-1-L)(j+Sum_j)+(j+Sum_j)^2\}
\]
\[f_i=\min_{1\le j<i}\{f_j+(j+Sum_j)^2-2(i+Sum_i-1-L)(j+Sum_j)+(i+Sum_i-1-L)^2\}
\]

\[f_j+(j+Sum_j)^2=2(i+Sum_i-1-L)(j+Sum_j)+f_i-(i+Sum_i-1-L)^2
\]

此时显然:

\[\begin{cases}
x=j+Sum_j\\
y=f_j+(j+Sum_j)^2\\
k=2(i+Sum_i-1-L)\\
b=f_i-(i+Sum_i-1-L)^2\\
\end{cases}
\]

然后就可以优化了。

代码

点击查看代码
namespace DP{
ll q[N]={0},hd=1,tl=1;
inline ll x(ll i){return i+sum[i];}
inline ll y(ll i){return f[i]+(i+sum[i])*(i+sum[i]);}
inline ll k(ll i){return 2*(i+sum[i]-1-L);}
inline ll b(ll i){return f[i]-(i+sum[i]-1-L)*(i+sum[i]-1-L);}
void Dp(){
memset(q,0,sizeof(q));
_for(i,1,n){
#define h(o) q[hd+o]
#define t(o) q[tl-o]
while(hd<tl&&(y(h(1))-y(h(0)))<=k(i)*(x(h(1))-x(h(0))))
++hd;
ll j=h(0);
f[i]=y(j)-k(i)*x(j)-b(i);
while(hd<tl&&(y(t(0))-y(t(1)))*(x(i)-x(t(0)))>(y(i)-y(t(0)))*(x(t(0))-x(t(1))))
--tl;
q[++tl]=i;
#undef h
#undef t
}
return;
}
}

[APIO2010]特别行动队

思路

随便推一推:

\[f_i=\max_{1\le j<i}\{f_j+a(Sum_i-Sum_j)^2+b(Sum_i-Sum_j)+c\}
\]
\[f_i=\max_{1\le j<i}\{f_j+a[(Sum_i)^2-2Sum_iSum_j+(Sum_j)^2]+b(Sum_i-Sum_j)+c\}
\]
\[f_i=\max_{1\le j<i}\{f_j+a(Sum_i)^2-2a\times Sum_iSum_j+a(Sum_j)^2+b\times Sum_i-b\times Sum_j+c\}
\]

\[f_j+a(Sum_j)^2-b\times Sum_j=(2aSum_i)\times Sum_j+(f_i-a(Sum_i)^2-b\times Sum_i-c)
\]

此时显然:

\[\begin{cases}
x=Sum_j\\
y=f_j+a(Sum_j)^2-b\times Sum_j\\
k=2aSum_i\\
b=f_i-a(Sum_i)^2-b\times Sum_i-c\\
\end{cases}
\]

然后就可以优化了。

注意:此时求的是最大值,也就是说维护的是一个上凸包。此时只要把下凸包往上翻一下,改一改大于号小于号就好了。

差不多长这样:

代码

点击查看代码
namespace DP{
ll q[N]={0},hd=1,tl=1;
inline ll X(ll i){return sum[i];}
inline ll Y(ll i){return f[i]+a*sum[i]*sum[i]-b*sum[i];}
inline ll K(ll i){return 2*a*sum[i];}
inline ll B(ll i){return f[i]-a*sum[i]*sum[i]-b*sum[i]-c;}
void Dp(){
memset(q,0,sizeof(q));
f[0]=0;
_for(i,1,n){
#define h(o) q[hd+o]
#define t(o) q[tl-o]
while(hd<tl&&(Y(h(1))-Y(h(0)))>=K(i)*(X(h(1))-X(h(0))))
++hd;
ll j=h(0);
f[i]=Y(j)-K(i)*X(j)-B(i);
while(hd<tl&&(Y(t(0))-Y(t(1)))*(X(i)-X(t(0)))<(Y(i)-Y(t(0)))*(X(t(0))-X(t(1))))
--tl;
q[++tl]=i;
#undef h
#undef t
}
return;
}
}

[ZJOI2007]仓库建设

思路

随便推一推:

\[f_i=\min_{1\le j<i}\{f_j+\sum_{k=j+1}^{i}\{p_{k}*(x_{i}-x_k)\}+c_i{}\}
\]
\[f_i=\min_{1\le j<i}\{f_j+\sum_{k=j+1}^{i}\{p_{k}*x_{i}-p_{k}*x_k\}+c_i{}\}
\]
\[f_i=\min_{1\le j<i}\{f_j+x_{i}*SumP_i-x_{i}*SumP_j-SumPX_i+SumPX_j+c_{i}\}
\]

\[f_j+SumPX_j=x_{i}*SumP_j+(f_i-x_{i}*SumP_i+SumPX_i-c_{i})
\]

此时显然:

\[\begin{cases}
x=SumP_j\\
y=f_j+SumPX_j\\
k=x_{i}\\
b=f_i-x_{i}*SumP_i+SumPX_i-c_{i}\\
\end{cases}
\]

然后就可以优化了。

但是这道题还有一个坑点:\(0\leq x_i,p_i,c_i<2^{31}\)。

也就是说有的山没有货物,有时我们可能不必在山脚建仓库,而是在一个之后的工厂都没有货物的工厂建仓库!

解决办法是加上下面的代码:

for_(i,n,1){
ans=min(ans,f[i]);
if(p[i])break;
}

代码

点击查看代码
namespace DP{
ll q[N]={0},hd=1,tl=1;
inline ll X(ll i){return sp[i];}
inline ll Y(ll i){return f[i]+spx[i];}
inline ll K(ll i){return x[i];}
inline ll B(ll i){return f[i]-x[i]*sp[i]+spx[i]-c[i];}
void Dp(){
memset(q,0,sizeof(q));
_for(i,1,n){
#define h(o) q[hd+o]
#define t(o) q[tl-o]
while(hd<tl&&(Y(h(1))-Y(h(0)))<=K(i)*(X(h(1))-X(h(0))))
++hd;
ll j=h(0);
f[i]=Y(j)-K(i)*X(j)-B(i);
while(hd<tl&&(Y(t(0))-Y(t(1)))*(X(i)-X(t(0)))>=(Y(i)-Y(t(0)))*(X(t(0))-X(t(1))))
--tl;
q[++tl]=i;
#undef h
#undef t
}
return;
}
}

[USACO08MAR]Land Acquisition G

思路

尝试直接推,但是发现推不出来。

首先可以发现一个性质:如果长方形 \(j\) 的长和宽都比长方形 \(i\) 的长和宽小,那么 \(j\) 可以直接并在 \(i\) 里面。

那么我们把这些土地以长为第一关键字,宽为第二关键字排序,把不会被完全覆盖的土地留下来,得到一些长单调递增,宽单调递减的土地。

如何实现这个操作?

排序之后长肯定是递增的。

如果一个宽比后面的小,那么它的长必然比后面的小,一定会被覆盖。

所以只要找出一个宽单调递减的序列就好了。

那么我们倒序遍历数组,记录当前的最大宽,如果当前宽比之前的最大宽大就加进去即可。

见代码GetLand部分。

然后就可以根据“长单调递增,宽单调递减”这个性质列出转移方程:

\[f_i=\min_{1\le j<i}\{f_{j}+w_{i}\times h_{j+1}\}
\]
\[f_{j}=-w_{i}\times h_{j}+f_i
\]

此时显然:

\[\begin{cases}
x=-h_j\\
y=f_j\\
k=w_i\\
b=f_i\\
\end{cases}
\]

然后就可以优化了。

代码

点击查看代码
namespace LAND{
inline bool cmp(Land a,Land b){
if(a.w==b.w)return a.h<b.h;
return a.w<b.w;
}
ll GetLand(){
sort(ld+1,ld+n+1,cmp);
ll len=0,max_h=0;
for_(i,n,1){
if(ld[i].h>max_h){
max_h=ld[i].h;
la[++len]=ld[i];
}
}
_for(i,1,len/2)swap(la[i],la[len-i+1]);
return len;
}
}
namespace DP{
ll q[N]={0},hd=1,tl=1;
inline ll X(ll i){return -la[i+1].h;}
inline ll Y(ll i){return f[i];}
inline ll K(ll i){return la[i].w;}
void Dp(){
memset(q,0,sizeof(q));
_for(i,1,n){
#define h(o) q[hd+o]
#define t(o) q[tl-o]
while(hd<tl&&(Y(h(1))-Y(h(0)))<=K(i)*(X(h(1))-X(h(0))))
++hd;
ll j=h(0);
f[i]=Y(j)-K(i)*X(j);
while(hd<tl&&(Y(t(0))-Y(t(1)))*(X(i)-X(t(0)))>(Y(i)-Y(t(0)))*(X(t(0))-X(t(1))))
--tl;
q[++tl]=i;
#undef h
#undef t
}
return;
}
}

\[\Huge{\mathfrak{The\ End}}
\]

学习笔记」斜率优化dp的相关教程结束。

《「学习笔记」斜率优化dp.doc》

下载本文的Word格式文档,以方便收藏与打印。