[斜率优化DP]噩梦的开始
引入
我认为的斜率优化本质就是讲状态转移方程转化为 的形式,并维护成一个凸包,用二分/CDQ/平衡树优化。
例1:任务安排1,2
任务安排1:LOJ 10184/Acwing300/P2365 任务安排
任务安排2:LOJ 10185/Acwing301/P5785 [SDOI2012]任务安排
两道题差不多,只是数据范围的区别,我们合在一起讲
题目大意
有 个任务排成一个序列,顺序不得改变,其中第 个任务的耗时为 , 费用系数为 。
现需要把这 个 任务分成若干批进行加工处理。
每批次的段头,需要额外消耗 的时间启动机器。每一个任务的完成时间是所在批次的结束时间。
完成一个任务的费用为:从 0 时刻到该任务所在批次结束的时间 t 乘以该任务费用系数 c。
分析
我们先列出最基本的状态转移方程。
状态表示 :前 个任务,且第 个任务是第 个批次的最后一个任务的方案。
状态属性 :方案贡献的最小值。
转移方程:
两维状态()加上一个决策变量(),时间复杂度为 。
优化状态转移方程
这里,我们可以用费用提前计算的经典优化思想进行优化:
在状态转移中,我们额外引入参数 ,仅仅是为了求出 对于当前状态的贡献。
既然是额外引入的,那就尝试把它去掉。若为 的任务开一个新的批次,那么该批次的启动时间实际会影响的任务有 。
那么我们不妨将该段 的 费用直接累加到当前状态 上计算。
一维状态加上一个决策变量,时间复杂度为
至此,我们便可以做出任务安排1。
斜率优化
我们将式子中单独含 的常量提出:
我们知道含 的项是常量,所以 可以转化为一下形式:
而变量1和变量2均是与 有关的变量,不妨令,则该函数可转化成 。
求 的极值问题,可以联想到直线的斜截式方程:。变形得 。
要求 的极值,就是求一个点 与当前 构成的所有直线中,截距最小的。
如图,黑色的点为所有 的点 ,红色线为斜率 的某条直线。
从下往上(截距由小到大)去逼近所有的点,则第一个出现在直线上的点,就是满足 的最小截距 。
但暴力查找这个点最坏是 的,太慢。
如上图,我们发现如果某个点不在凸壳上,则不可能对答案产生贡献。
对于这道题,我们需要维护下凸壳。因此,对于任意 ,只用在下凸壳的点寻找构成直线的最小截距。
但在最坏的情况下查找还是 的,考虑继续优化。
由于 都是正整数,所以它们的前缀和 一定是单调递增的。对应 也是单调递增的。
而下凸壳中相邻两点的斜率也单调递增,可得对于第一个出现在直线上的点,一定有 。
又由于 单调递增,所以 之前的点都不会是点集中第一个出现在直线上的点。
只需维护点集区间 之间的即可,知道 ,维护区间转为 。
我们可以发现一个熟悉的滑动窗口模型,考虑用单调队列维护:
- 用队头的两个元素维护大于 的最小斜率 。
- 插入前,保证队列中至少两个点,然后把满足 的点 弹出。
这样便维护了有效下凸壳点集,时间复杂度 。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 3e5 + 10;
int n,S,q[N];
ll t[N],c[N],f[N];
int main(){
scanf("%d%d",&n,&S);
for(int i=1;i<=n;i++){
scanf("%lld%lld",t+i,c+i);
t[i]+=t[i-1],c[i]+=c[i-1];
}
int hh=0,tt=0;q[0]=0;// 队列中第一个点是0
/*
f[i]=min(f[j]+S*(c[n]-c[j])+t[i]*(c[i]-c[j])) =>
f[i]=t[i]*c[i]+S*c[n]+min(f[j]-(S+t[i])*c[j])
y=kx+b ==> b=y-kx
f[j]-(S+t[i])*c[j]=y-kx = min(b)
k[qhh~qh+1]=y1-y2/x1-x2=(f[qh+1]-f[qhh])/(c[qh+1]-c[qhh]) > ki = t[i]+S
k[qt-1~qtt] < k[qtt~i]
*/
for(int i=1;i<=n;i++){
while(hh<tt/*至少还有两个点*/&&(f[q[hh+1]]-f[q[hh]])/*y*/<=(c[q[hh+1]]-c[q[hh]])/*x*/*(t[i]+S)/*k*/) hh++;
f[i]=f[q[hh]]+S*(c[n]-c[q[hh]])+t[i]*(c[i]-c[q[hh]]);
while(hh<tt&&(f[q[tt]]-f[q[tt-1]])*(c[i]-c[q[tt]])>=
(f[i]-f[q[tt]])*(c[q[tt]]-c[q[tt-1]])) tt--;
q[++tt]=i;
}
printf("%lld\n", f[n]);
return 0;
}
例2:任务安排3
注意到此题与前两道题唯一的差别便是 不一定是正整数,即 不一定单调递增。
这意味着我们要将下凸壳中的所有点保存下来,不因为 出队。查找时二分答案,总时间复杂度为 。
代码就不贴了。
习题
本文作者:ASnown
本文链接:https://www.cnblogs.com/As-Snow/p/17254119.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步