ybtAu 「动态规划」第5章 斜率优化 DP
这是 neatisaac 的金牌导航题解!
提示:部分代码中存在细节问题,不影响通过,但可能影响理解。
A. 【例题1】任务安排1
令 \(f_{i,j}\) 表示前 \(i\) 个任务分 \(j\) 段的最小费用,\(T_i\) 表示前 \(i\) 项任务的时间和,\(C_i\) 表示前 \(i\) 项任务的费用系数和,容易得出暴力转移方程:
时间复杂度 \(O(n^3)\),无法通过。于是对它进行优化。
发现加一段对后面时间造成的影响是一定的,于是将状态的 \(j\) 这一维去掉,得:
时间复杂度 \(O(n^2)\),可以通过。
这启示我们可以把对后面的贡献加到前面去。
#include <iostream>
#define N 300005
int n,s,t[N],c[N],q[N],f[N],tl;
double X(int x) {return c[x];}
double Y(int x) {return f[x]-s*c[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
int bs(double x)
{
if(tl==1) return 0;
int l=1,r=tl;
while(l<r)
{
int mid=l+r>>1;
if(slope(q[mid],q[mid+1])<=x) l=mid+1;
else r=mid;
}
return q[l];
}
int main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
std::cin>>n>>s;
for(int i=1;i<=n;i++) std::cin>>t[i]>>c[i],t[i]+=t[i-1],c[i]+=c[i-1];
tl=1;
for(int i=1;i<=n;i++)
{
int p=bs(t[i]);
f[i]=f[p]+t[i]*(c[i]-c[p])+s*(c[n]-c[p]);
while(tl>1&&slope(q[tl-1],q[tl])>slope(q[tl-1],i)) tl--;
q[++tl]=i;
}
std::cout<<f[n];
}
B. 【例题2】任务安排2
现在 \(O(n^2)\) 也过不了了。于是对它进行优化。
整理上式,得到:
将先前的决策点抽象为二维平面上的点,问题抽象为求斜率一定且过这些点的直线的最小截距,即:
其中:
而要想使截距最小,不需要遍历所有的点,只需要找这些点组成的一个凸壳,并找到斜率为 \(T_i\) 的直线与它的切点。
发现凸壳的斜率是单调递增的,而所找的 \(T_i\) 也是单调递增的。于是可以利用类似双指针的做法(也称单调队列)来解决这个问题。
#include <iostream>
#define N 10005
int n,s,t[N],c[N],f[N],q[N];
double X(int x) {return c[x];}
double Y(int x) {return f[x]-s*c[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
int main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
std::cin>>n>>s;
for(int i=1;i<=n;i++) std::cin>>t[i]>>c[i],t[i]+=t[i-1],c[i]+=c[i-1];
int hd=1,tl=1;
for(int i=1;i<=n;i++)
{
double k=t[i];
while(hd<tl&&slope(q[hd],q[hd+1])<k) hd++;
f[i]=f[q[hd]]+t[i]*(c[i]-c[q[hd]])+s*(c[n]-c[q[hd]]);
while(hd<tl&&slope(q[tl-1],q[tl])>slope(q[tl-1],i)) tl--;
q[++tl]=i;
}
std::cout<<f[n];
}
C. 【例题3】任务安排3
虽然时间为负在现实中不可能,但是题还是这么出了。
由于 \(T_i\) 不再单调递增,所以需要在凸壳上二分而非双指针单调队列来找切点。
本题需要注意细节,不要把 \(\ge\) 写成 \(>\)。
#include <iostream>
#define int long long
#define N 300005
int n,s,t[N],c[N],q[N],f[N],tl;
double X(int x) {return c[x];}
double Y(int x) {return f[x]-s*c[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
int bs(double x)
{
if(tl==1) return 0;
int l=1,r=tl;
while(l<r)
{
int mid=l+r>>1;
if(slope(q[mid],q[mid+1])<=x) l=mid+1;
else r=mid;
}
return q[l];
}
signed main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
std::cin>>n>>s;
for(int i=1;i<=n;i++) std::cin>>t[i]>>c[i],t[i]+=t[i-1],c[i]+=c[i-1];
tl=1;
for(int i=1;i<=n;i++)
{
int p=bs(t[i]);
f[i]=f[p]+t[i]*(c[i]-c[p])+s*(c[n]-c[p]);
while(tl>1&&slope(q[tl-1],q[tl])>=slope(q[tl-1],i)) tl--;
q[++tl]=i;
}
std::cout<<f[n];
}
D. 【例题4】猫的运输
发现一个饲养员能领走猫 \(i\),需要出发时间 \(t\ge T_i-\sum_{j=2}^{H_i} D_j\),而等待时间就是 \(t-(T_i-\sum_{j=2}^{H_i}D_j)\)。
于是令 \(A_i=T_i-\sum_{j=2}^{H_i}D_j\),将所有猫按照 \(A_i\) 排序,一个饲养员领走的猫在这个序列上一定是连续的。而该饲养员出发的时间就是这个区间最右面的 \(A_i\)。
令 \(f_{i,j}\) 表示前 \(i\) 个饲养员领走前 \(j\) 只猫所需的最小等待时间和,有:
其中 \(S_i=\sum_{j\le i}A_j\)。
变成了斜率优化的标准形式,斜率为 \(A_j\)。
由于 \(A_j\) 是单调递增的,且凸壳斜率单调递增,所以可以用单调队列做法。
#include <iostream>
#include <cstring>
#include <algorithm>
#define N 100005
#define int long long
int n,m,p,d[N],a[N],f[105][N],s[N],i,q[N];
double X(int x) {return x;}
double Y(int x) {return f[i-1][x]+s[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
signed main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
memset(f,0x3f,sizeof f);
std::cin>>n>>m>>p;
for(i=2;i<=n;i++) std::cin>>d[i],d[i]+=d[i-1];
for(i=1;i<=m;i++)
{
int h,t;
std::cin>>h>>t;
a[i]=t-d[h];
}
std::sort(a+1,a+m+1);
for(i=1;i<=m;i++) s[i]=s[i-1]+a[i];
f[0][0]=0;
for(i=1;i<=p;i++)
{
int hd=1,tl=1;
q[1]=0;
for(int j=1;j<=m;j++)
{
double k=a[j];
while(hd<tl&slope(q[hd],q[hd+1])<k) hd++;
int o=q[hd];
f[i][j]=std::min(f[i-1][j],f[i-1][o]+a[j]*(j-o)-(s[j]-s[o]));
while(hd<tl&&slope(q[tl-1],q[tl])>slope(q[tl-1],j)) tl--;
q[++tl]=j;
}
}
std::cout<<f[p][m];
}
E. 特别行动队
板子。
令 \(f_i\) 表示前 \(i\) 个队员的最大修正战力值,\(X_i\) 表示前 \(i\) 个人初始战力值之和,有:
变成了斜率优化标准形式,斜率为 \(2aX_i\)。
与前几题不同,这里维护的是最大值,所以凸壳上斜率是单调递减的,而由于 \(a<0\),所以询问的斜率也是单调递减的,可以使用单调队列来做。
#include <iostream>
#define int long long
#define N 1000005
int n,a,b,c,w[N],f[N],q[N];
double X(int x) {return w[x];}
double Y(int x) {return f[x]+a*w[x]*w[x]-b*w[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
signed main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
std::cin>>n>>a>>b>>c;
for(int i=1;i<=n;i++) std::cin>>w[i],w[i]+=w[i-1];
int hd=1,tl=1;
for(int i=1;i<=n;i++)
{
double k=2*a*w[i];
while(hd<tl&&slope(q[hd],q[hd+1])>k) hd++;
f[i]=f[q[hd]]+a*(w[i]-w[q[hd]])*(w[i]-w[q[hd]])+b*(w[i]-w[q[hd]])+c;
while(hd<=tl&&slope(q[tl-1],q[tl])<slope(q[tl-1],i)) tl--;
q[++tl]=i;
}
std::cout<<f[n];
}
F. 打印文章
板子。
令 \(f_i\) 表示前 \(i\) 个单词的最少花费,\(C_i\) 表示前 \(i\) 个单词的费用和。于是有:
变成了斜率优化的标准形式,斜率为 \(2C_i\)。
\(2C_i\) 单调递增,且凸壳斜率单调递增,使用单调队列。
注意细节,单词长度可能为 \(0\)。
有多测。
#include <iostream>
#define int long long
#define N 500005
int n,m,a[N],q[N],f[N];
double X(int x) {return a[x];}
double Y(int x) {return f[x]+a[x]*a[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
signed main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
while(std::cin>>n>>m)
{
a[0]=f[0]=0;
for(int i=1;i<=n;i++) std::cin>>a[i],a[i]+=a[i-1],f[i]=0;
int hd=1,tl=1;
q[1]=0;
for(int i=1;i<=n;i++)
{
double k=2*a[i];
while(hd<tl&&slope(q[hd],q[hd+1])<k) hd++;
f[i]=f[q[hd]]+(a[i]-a[q[hd]])*(a[i]-a[q[hd]])+m;
while(hd<tl&&slope(q[tl-1],q[tl])>=slope(q[tl-1],i)) tl--;
q[++tl]=i;
}
std::cout<<f[n]<<'\n';
}
}
G. 锯木厂选址
注意到我们只需要建两个锯木厂。
令 \(f_i\) 表示在 \(i\) 建第二个锯木厂的最小运输费用,\(D_i\) 表示第 \(i\) 棵树到山脚的距离,\(W_i\) 表示前 \(i\) 棵树的重量和,\(sum\) 表示所有木材运到山脚的运输费用。枚举第一个锯木厂的位置,运用容斥的思想,可得:
解释一下,\(D_jW_j\) 表示前 \(j\) 棵树从 \(j\) 运到山脚的费用,\(D_i(W_i-W_j)\) 表示第 \(j+1\) 棵树到第 \(i\) 棵树从 \(i\) 运到山脚的费用。
展开得到:
变成了斜率优化的标准形式,斜率为 \(D_i\),单调递增,且维护的是最大值,所以可以用单调队列实现。
#include <iostream>
#define int long long
#define N 20005
int n,w[N],d[N],sum,q[N];
double X(int x) {return w[x];}
double Y(int x) {return -d[x]*w[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
signed main()
{
std::ios::sync_with_stdio(0);
std::cin.tie(0),std::cout.tie(0);
std::cin>>n;
for(int i=1;i<=n;i++) std::cin>>w[i]>>d[i];
for(int i=n;i>=1;i--) d[i]+=d[i+1],sum+=w[i]*d[i];
for(int i=1;i<=n;i++) w[i]+=w[i-1];
int hd=1,tl=0,ans=0;
for(int i=1;i<=n;i++)
{
double k=-d[i];
while(hd<tl&&slope(q[hd],q[hd+1])<k) hd++;
if(hd<=tl) ans=std::max(ans,d[q[hd]]*w[q[hd]]+d[i]*(w[i]-w[q[hd]]));
while(hd<tl&&slope(q[tl-1],q[tl])>=slope(q[tl-1],i)) tl--;
q[++tl]=i;
}
std::cout<<sum-ans;
}

浙公网安备 33010602011771号