最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划
题目描述
有 n 个 任务 排成一个 序列,顺序不得改变,其中第 i 个 任务 的 耗时 为 ti, 费用系数 为 ci
现需要把该 n 个 任务 分成 若干批 进行加工处理
每批次的 段头,需要 额外消耗 S 的时间启动机器。每一个任务的 完成时间 是所在 批次 的 结束时间。
完成一个任务的 费用 为:从 0 时刻 到该任务 所在批次结束 的时间 t 乘以 该任务 费用系数 c
分析
关于本题朴素版的DP分析,可以看该篇博客:AcWing300. 任务安排1【线性DP+费用提前计算思想】
贴出朴素版的 状态转移方程:
fi=min
提出式子中 含有单独 i 的常量:f_i = st_i\times sc_i + S \times sc_n + \min\bigg(f_j - S \times sc_j - st_i \times sc_j\bigg)
考察 min 函数内部的 多项式:f_j - sc_j \times (S + st_i)
该式子中,有 i \times j 的项,因此不能直接用上一章节中提到的 滑动窗口 来维护一个前缀的 最值
但是 分析的思想 可以继承,我们知道 含 i 的项是一个 常量,故该 多项式 就能够 抽象 成如下形式:
f_j - sc_j \times (S + st_i) = 变量1 - 变量2 \times (常量S + 常量i)
注意,这里的 变量1 和 变量2 并不是两个 独立变量 (该 函数 非 多元函数)
变量1 f_j 是与 j 有关的 变量,变量2 sc_j 也是与 j 有关的 变量
因此,不妨令 \begin{cases} f_j = y(j) \\\\ sc_j = x(j) \\\\ k = S+st_i \end{cases},则该函数可以化为:y(j) - k \cdot x(j)
为了式子 方便观察,接下来我会把 y(j) 写成 y,x(j) 写成 x,但是请读者心中明白,这两个 变量,都是关于 j 的 变量(学过 高数 的,直接类比 参数方程 即可)
求 y - kx ~ (0 \le j \lt i) 函数的极值问题,可以直观想到 直线的斜截式方程:y = kx + b
对 直线的斜截式方程 进行变形:b = y - kx
要求 y - kx ~ (0 \le j \lt i) 函数的极值,就是求一个点 (x_j, y_j) 与当前 k_i 构成的 所有直线 中,y轴截距最小
为了方便各位读者理解,在这贴上一幅图,为大家 几何直观 的展示这一过程:
正如华罗庚先生说过的:数缺形时少直观,形少数时难入微,数形结合百般好
图中,黑色点 为所有 0 \le j \lt i 的点 (x_j,y_j),红色线
为 斜率 是 k_i 的 某一条直线
我们 从下往上(截距从小到大) 去逼近当前 所有的点
则 第一个 出现在直线上的点,就是满足 b_i = y_j - kx_j 的 最小截距 b_i,即是当前阶段 DP 的 最佳策略
那么,如何有效的维护点集呢?
这就是一个 线性规划 的问题了:在 点集 中,找到一个 点,绘制 固定斜率 的直线,使得 截距最小
由 线性规划 知识可知:我们只用考虑 点集 中 凸壳 上的点即可
几何直观 上,显然这题要维护的是 下凸壳
因此,对于任意的 f_i 来说,我们只需去寻找 下凸壳 上的 点 构成 直线 的 最小截距 即可
这样 时间复杂度 在 最坏 的情况下,还是 O(n^2)(即 所有点 的 (x,y) 单增,全部点 构成一个 下凸壳)
斜率优化了个寂寞?并非如此!我们再看看直线方程中,各参数的性质
由于 t_i, c_i 都是 正整数,故他们的 前缀和 st_i,sc_i 是 单调递增的
对应于 直线方程的参数:x_j = sc_j 是 单调递增 的,k_i = S + st_i 也是 单调递增 的
而 下凸壳 中 相邻两点 构成的直线 斜率 也是 单调递增 的
则 下凸壳 中第一个出现在 直线 上的点,满足:k_{j-1, j} \le k_i \lt k_{j,j+1},此时 直线截距 b_j 最小
大家可以配合下图几何直观理解该处的不等式
而又由于 k_i 单调递增,所以 j 之前 的 点 都不会是 点集 中出现在 直线 上的 第一个点
此时只需 维护点集区间 [j,i] 的 点 即可,直到 k_{j,j+1} \le k \lt k_{j+1,j+2} 时,维护点集区间 变为 [j+1,i]
根据上述所说,出现了我们最熟悉的模型-滑动窗口模型。
故我们可以直接利用 单调队列 来维护 下凸壳中的有效点集
并用队头的 两个元素 维护 大于当前斜率 k_i 的最小斜率 k_{q_{hh}~,~q_{hh+1}}
这里我把公式展开,方便大家理解:
k_{q_{hh}~,~q_{hh+1}} \gt k_i \Rightarrow \frac{y_{q_{hh+1}}-y_{hh}}{x_{q_{hh+1}}-x_{hh}} \gt k_i \Rightarrow \frac{f_{q_{hh+1}}-f_{hh}}{sc_{q_{hh+1}}-sc_{hh}} \gt S+st_i
把点插入 单调队列 前,先要 队列 中 至少有两个点,然后把 满足 k_{q_{tt-1}~,~q_{tt}} \ge k_{q_{tt}~,~i} 的 点 q_{tt} 弹出
即 新加入的点,必须和 原点集 构成 下凸壳,无效点要先删去
这里我把公式展开,方便大家理解:
k_{q_{tt-1}~,~q_{tt}} \lt k_{q_{tt}~,~i} \Rightarrow \frac{y_{q_{tt}}-y_{q_{tt-1}}}{x_{q_{tt}}-x_{q_{tt-1}}} \lt \frac{y_{i}-y_{q_{tt}}}{x_{i}-x_{q_{tt}}} \Rightarrow \frac{f_{q_{tt}}-f_{q_{tt-1}}}{sc_{q_{tt}}-sc_{q_{tt-1}}} \lt \frac{f_{i}-f_{q_{tt}}}{sc_{i}-sc_{q_{tt}}}
这样,队列 中 相邻两点 之间构成的直线 斜率单增,也就是我们的 有效下凸壳点集
斜率优化DP模板(来源:Oi-Wiki)
上面密密麻麻零零散散写了一堆分析,最后我们来把 斜率优化 总结成一个 模板,方便大家记忆:
- 将初始状态入队
- 每次使用一条和 i 相关的直线 f_i 去切维护的凸包,找到最优决策,更新 dp_i
- 加入状态 dp_i。如果一个状态(即凸包上的一个点)在 dp_i 加入后不再是凸包上的点,需要在 dp_i 加入之前剔除
斜率优化往往还要再套一层 二分/CDQ/平衡树优化
不过本题直接用单调队列就好了,上述优化方法的题目会在今后的博客中提到(有生之年)
Code
时间复杂度: O(n)
这题非常的好,希望大家不要抄代码,好好推一遍 w
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 3e5 + 10;
int n, S, que[N];
LL st[N], sc[N], f[N];
int main()
{
scanf("%d%d", &n, &S);
for (int i = 1; i <= n; i ++ )
{
scanf("%lld%lld", &st[i], &sc[i]);
st[i] += st[i - 1], sc[i] += sc[i - 1];
}
int hh = 0, tt = 0; que[0] = 0;
for (int i = 1; i <= n; i ++ )
{
while (hh < tt && (f[que[hh + 1]] - f[que[hh]]) <=
(sc[que[hh + 1]] - sc[que[hh]]) * (st[i] + S))
hh ++ ;
f[i] = f[que[hh]] + S * (sc[n] - sc[que[hh]]) + st[i] * (sc[i] - sc[que[hh]]);
while (hh < tt && (f[que[tt]] - f[que[tt - 1]]) * (sc[i] - sc[que[tt]]) >=
(f[i] - f[que[tt]]) * (sc[que[tt]] - sc[que[tt - 1]]))
tt -- ;
que[ ++ tt] = i;
}
printf("%lld\n", f[n]);
return 0;
}
彩铅好强,什么都会
看成了彩笔(
dalao,维护队头的时候为什么不能这么写啊,提个负号出来是一样的,为啥就WA了😭
while (hh < tt && (f[que[hh]] - f[que[hh + 1]]) <= (sc[que[hh]] - sc[que[hh + 1]]) * (st[i] + S)) hh ++ ;
<=修改成>=就行了,相当于不等式左右都乘一个-1,这样不等号需要换方向。
,,,脑子短路了
谢谢大哥
%%%
再也看不到彩铅发题解了
注意有些地方写错了
hh->q[hh]
你是我的神
6
真的强
tql
请问队列初始化的时候,为什么要先加入一个 0?tt=-1,hh=0。判断条件是 hh <= tt 就过不了。
因为也可以从 0 转移过来呀
while (hh < tt && (f[que[hh + 1]] - f[que[hh]]) <= (sc[que[hh + 1]] - sc[que[hh]]) * (st[i] + S)) hh ++ ;
为啥这里是小于等于,斜率相等的时候说明两直线重和,那最先碰到的点应该就是队头这个点呀,怎么还出队了
我改成<也ac了
如果直线重合,就是说点集中有两个点与直线方程符合,取哪一个,都可以得到同样的最小截距
但是while循环出队不就把这两个点都出掉了咩
hh<tt 会保留两个,是不是这样会造成错误呢?我猜的,瞎说的,如果一般情况hh<=tt应该可以,但这里需要保留最少两个
不太懂你意思
可以选择保留,也可以选择不保留
想问下图是怎么画的呀
而又由于 ki 单调递增
错了哦题主的意思应该是最后形成的图是一个ki单调递增的下凸包,这个是维护形成的不是原来就是
巨巨,斜率优化是不是基本上都是一维的转移方程?然后再看是否满足条件
彩铅大佬,请问最后的两个式子里面好像好多地方的下标都应该是 q_{hh} 而非 hh 吧?
%%%
%%%%
%%%
好耶, 想明白了!!!
%%%%
%%%++