前言
由于某些原因,最近学习了这个优化 ( ext{DP}) 的算法——斜率优化。
算法简介
我们在做 ( ext{DP}) 的题目时,常常会遇到这样的一个状态转移方程:
对于其中的 (f(i),g(j)) 我们都可以考虑通过单调队列来优化掉。
但是对于 (h(i) imes r(j)) 这项,它与 (i,j) 同时有关,单调队列便不再适用……
于是,我们引进一个新的优化方式——斜率优化。
斜率优化,指的就是利用一次函数的斜率 (k) 相关性质对原来的状态转移方程进行优化。
而这就需要我们运用数形结合的思想来解题。
接下来将通过几道题目来加强对斜率优化的认识。
例题讲解
下面提供几道较为基础的题目:
例题1:[HNOI2008]玩具装箱
(large{ ext{Description:}})
现在有 (n) 个玩具,第 (i) 个玩具的长度为 (c[i]) ,将 (i) 至 (j) 连续的玩具放入一个容器,其长度为 (x=j-i+sumlimits_{k=i}^{j}c[k]) ,制作一个容器的费用为 (left(x-L ight)^{2}) ,其中 (L) 为常数。
求将所有玩具都放入容器中的费用最小值,容器的数量不限。
数据范围: (nleq 5 imes 10^{4},1leq L,c[i]leq 10^{7})
(large{ ext{Solution:}})
首先,我们不难想到用 ( ext{DP}) 来求解。
设 (dp[i]) 表示将 (1) 至 (i) 的玩具都放入容器中的最小费用,于是可以得到如下的状态转移方程:
设 (c) 的前缀和数组为 (s) ,即 (s[i]=sumlimits_{k=1}^{j}c[k]) ,于是方程就变为如下形态:
此时,如果我们暴力去做,时间复杂度为 (Oleft(n^{2} ight)) ,并不够优秀。
于是我们考虑对其进行斜率优化。
显然,状态 (i) 是由一个状态 (j) 转移而来,而这个 (j) 是当前最优决策,于是我们考虑两个决策 (j_1,j_2left(1leq j_1,j_2<i ight)) ,且决策 (j_1) 优于决策 (j_2) ,那么有如下式子:
为了方便起见,我们设 (sum[i]=s[i]+ileft(1leq ileq n ight)) ,此时我们化简上述式子得:
在这里,我们把 (sum[j]) 看作横坐标 (x_j) ,把 (dp[j]+left(sum[j]+L+1 ight)^2) 看作纵坐标 (y_j) ,那么上述式子的右侧就相当于 (egin{aligned}frac{Delta y}{Delta x}=kend{aligned}) ,也就是一次函数的斜率!
此时对于只与 (i) 有关的项,我们可以把它当成一次函数的截距 (b) ,那么所求就变成了 (b) 的最小值。
我们是如何想到把式子化简成上面的样子呢?
实际上,我们把所有只与 (i) 有关的项合并,当作一次函数的 (b) ;再把所有只与 (j) 有关的项合并,当作一次函数的 (y) ;最后把所有与 (i,j) 都有关的项再合并,当作一次函数的 (kx) 。
在计算的过程中,我们发现 (b) 消掉了,剩下的就是上面的式子。
当我们在求 (dp[i]) 时, (k=2 imes sum[i]) 显然不变。
也就是说,我们平移一条斜率为定值 (k) 的直线,使得该直线过某点 (left(x_j,y_j ight)) ,那么此时的截距就是一个可行解。
由于求的是最小值,故我们从下往上平移该直线,第一次得到的可行解就是最优解。
显然,斜率 (k) 是单调递增的,于是我们用单调队列维护一个 (left(x_j,y_j ight)) 的下凸包。(这点容易证明是正确的)
这个时候所得的队首就是对于 (i) 而言的最佳决策。
(large{ ext{Code:}})
#include<bits/stdc++.h>
#define Re register
using namespace std;
typedef long long LL;
typedef double db;
const int N=50005;
int n,L;
LL sum[N];
int h,t; LL q[N];
LL dp[N];
inline LL rd()
{
char ch=getchar();
LL x=0,f=1;
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
inline LL X(int x){return sum[x];}
inline LL Y(int x){return dp[x]+(sum[x]+L)*(sum[x]+L);}
inline LL K(int x){return 2*sum[x];}
inline db slope(int x,int y){return double(Y(y)-Y(x))/(X(y)-X(x));}
int main()
{
scanf("%d%d",&n,&L); L++;
for(Re int i=1;i<=n;i++)
{
sum[i]=sum[i-1]+rd()+1;
}
for(Re int i=1;i<=n;i++)
{
while(h<t&&slope(q[h],q[h+1])<=K(i)) h++;
dp[i]=dp[q[h]]+(sum[i]-sum[q[h]]-L)*(sum[i]-sum[q[h]]-L);
while(h<t&&slope(q[t],q[t-1])>=slope(q[t],i)) t--;
q[++t]=i;
}
printf("%lld",dp[n]);
return 0;
}
例题2:[CEOI2004]锯木厂选址
(large{ ext{Description:}})
从山顶上到山底下沿着一条直线种植了 (n) 棵老树。当地的政府决定把他们砍下来,并运送到锯木厂,而且木材只能朝山下运。
山脚下有一个锯木厂,另外两个锯木厂将新修建在山路上,你必须决定在哪里修建这两个锯木厂,使得运输的费用总和最小。
给定 (n) 棵树的重量与位置,计算最小运输费用。
数据范围: (nleq 20000)
(large{ ext{Solution:}})
这不难想到是个 ( ext{DP}) 题。
设 (dp[i]) 表示第 (2) 个工厂修到第 (i) 棵树的位置时的最小花费。
于是得出状态转移方程:
其中, (res) 表示所有树一开始全部运送的山脚下的花费, (sd) 表示距离后缀和, (sw) 表示重量前缀和。
然后我们将状态转移方程变形,令 (j_1,j_2) 这两种决策转移到 (i) 的时候, (j_1) 决策更优,那么我们就可以得到:
化简后得到:
此时令:
之后套上斜率优化的模板即可。
(large{ ext{Code:}})
#include<bits/stdc++.h>
#define Re register
using namespace std;
const int N=20005;
struct Node {
int w,d;
}a[N];
int n,sd[N],sw[N],res;
int h,t,q[N],ans;
inline int rd()
{
char ch=getchar();
int x=0,f=1;
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
inline int X(int x){return sw[x];}
inline int Y(int x){return sd[x]*sw[x];}
inline int K(int x){return sd[x];}
inline double slope(int x,int y){return 1.0*(Y(y)-Y(x))/((X(y)-X(x)));}
int main()
{
scanf("%d",&n);
for(Re int i=1;i<=n;i++)
{
a[i].w=rd();
a[i].d=rd();
}
for(Re int i=n;i>0;i--)
{
sd[i]=sd[i+1]+a[i].d;
}
for(Re int i=1;i<=n;i++)
{
sw[i]=sw[i-1]+a[i].w;
}
for(Re int i=1;i<=n;i++)
{
res+=a[i].w*sd[i];
}
ans=0x3f3f3f3f;
for(Re int i=1;i<=n;i++)
{
while(h<t&&slope(q[h],q[h+1])>K(i)) h++;
ans=min(ans,res-sd[q[h]]*sw[q[h]]-sd[i]*(sw[i]-sw[q[h]]));
while(h<t&&slope(q[t],q[t-1])<slope(q[t],i)) t--;
q[++t]=i;
}
printf("%d",ans);
return 0;
}
总结
-
斜率单调暴力移指针
-
斜率不单调二分找答案
-
(x) 坐标单调开单调队列
-
(x) 坐标不单调用平衡树或 ( ext{cdq}) 分治