\(AcWing\) \(302\) 任务安排\(3\)
一、题目描述
有 \(n\) 个 任务 排成一个 序列,顺序不得改变,其中第 \(i\) 个 任务 的 耗时 为 \(t_i\), 费用系数 为 \(c_i\)
现需要把该 \(n\) 个 任务 分成 若干批 进行加工处理
每批次的 段头,需要 额外消耗 \(S\) 的时间启动机器。每一个任务的 完成时间 是所在 批次 的 结束时间。
完成一个任务的 费用 为:从 \(0\) 时刻 到该任务 所在批次结束 的时间 \(t\) 乘以 该任务 费用系数 \(c\)
二、题目分析
本题 相较于 上一题 的不同之处在于:\(−512≤t_i≤512\)
该限制使得 \(t_i\) 的 前缀和 \(st_i\) 不再是 单调递增 的了
我们再来观察一下上一篇中推导的公式:
提出常量后的剩余部分:\(\large f_j-sc_j \times (S+st_i)\)
换元:\(\large y−kx\)
此处的换元是令
在 上一篇题解 中提到过,点集 上第一个出现在直线 \(y=kx+b\) 上的点是 下凸壳 上的点
且满足 \(k_{j−1,j}≤k_i<k_{j,j+1}\)
下凸壳 上的点集,相邻两点 构成的 斜率 是 单调递增 的
在上题中,斜率 \(k(k_i=S+st_i)\) 也是 单调递增 的,故可以用 单调队列 在 队头 维护 大于\(k\) 的 最小值
而本题中,\(k_i\) 不具备 单调性,因此不能再用 单调队列 优化了
不过, “下凸壳上的点集,相邻两点构成的斜率是单调递增的”
我们可以利用上 单调性,维护一个 下凸壳的点集,则对于 \(k_i\),找到 大于他的最小值 就可以 二分 啦
通过利用一个 队列(非 滑动窗口,故不考虑队列最大长度),完成对于 下凸壳点集 的维护即可
关于如何利用 队列 维护 下凸壳的点集,这在上篇题解中的最后有提到,直接 引用原文 了:
把点插入 队列 前,先要 队列 中 至少有两个点,然后把 满足 \(k_{q_{tt−1}, q_{tt}} ≥k_{q_{tt},i}\) 的 点 \(q_{tt}\) 弹出
即 新加入的点,必须和 原点集 构成 下凸壳,无效点要先删去
这里我把公式展开,方便大家理解:
\(\large \displaystyle k_{q_{tt-1},q_{tt}}<k_{q_{tt,i}} \Rightarrow \frac{y_{q_{tt}}-y_{q_{tt-1}}}{x_{q_{tt}}-x_{q_{tt-1}}}<\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}}}<\frac{f_i-f_{q_{tt}}}{sc_{q_i}-sc_{q_{tt}}}\)
这样,队列 中 相邻两点 之间构成的直线 斜率单增,也就是我们的 有效下凸壳点集
总结一下本题要点:
- 用队列维护 下凸壳点集
- 用 二分 找出 点集 中第一个出现在直线上的点
二、二分模板
#include <bits/stdc++.h>
using namespace std;
int n, m;
const int N = 110;
/*
前提:q是一个有序递增的数组,当容器中的元素按照递增的顺序存储时,
*/
int q[N] = {1, 2, 3, 3, 3, 4, 5};
int l, r;
//二分算法之手动版本
int manual_lower_bound(int l, int r, int x) {
while (l < r) {
int mid = (l + r) / 2;
if (q[mid] >= x)
r = mid;
else
l = mid + 1;
}
return l;
}
int manual_upper_bound(int l, int r, int x) {
while (l < r) {
int mid = (l + r) / 2;
if (q[mid] > x)
r = mid;
else
l = mid + 1;
}
return l;
}
int main() {
/**********************************************************/
//方法1:yxc 大法
//从0~6找出>=3的第一个位置
l = 0, r = 6;
int x = 3;
while (l < r) {
int mid = (l + r) >> 1;
if (q[mid] >= x)
r = mid;
else
l = mid + 1;
}
printf("%d\n", l);
//从0~6找出<=3的最后一个位置
l = 0, r = 6;
x = 3;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (q[mid] <= x)
l = mid;
else
r = mid - 1;
}
printf("%d\n", l);
/*
方法2:STL lower_bound,upper_bound 大法
lower_bound函数返回容器中第一个大于等于目标值的位置
upper_bound函数返回容器中第一个大于目标值的位置
若容器中的元素都比目标值小则返回最后一个元素的下一个位置
*/
printf("%lld\n", lower_bound(q, q + 7, x) - q);
printf("%lld\n", upper_bound(q, q + 7, x) - q - 1);
/**********************************************************/
/*
方法3:手写 lower_bound,upper_bound大法
*/
printf("%d\n", manual_lower_bound(0, 7, x));
printf("%d\n", manual_upper_bound(0, 7, x) - 1);
/**********************************************************/
return 0;
}
三、实现代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 300010;
int n, s;
ll st[N], sc[N], f[N];
int q[N];
int main() {
cin >> n >> s;
for (int i = 1; i <= n; i++) cin >> st[i] >> sc[i], st[i] += st[i - 1], sc[i] += sc[i - 1];
//初始化队列
int hh = 0, tt = 0; //添加哨兵
for (int i = 1; i <= n; i++) { //动态规划,从小到大枚举每个i
int l = hh, r = tt;
//二分模板lower_bound
//通过二分,找到第一个斜率大于k=st[i] + S的两个点,起点就是切点
while (l < r) {
int mid = (l + r) / 2;
// check函数
if (f[q[mid + 1]] - f[q[mid]] > (st[i] + s) * (sc[q[mid + 1]] - sc[q[mid]]))
r = mid;
else
l = mid + 1;
}
int j = q[l]; //切点位置
//动态规划
f[i] = f[j] - (st[i] + s) * sc[j] + st[i] * sc[i] + s * sc[n];
//出队尾,斜率比自己大的点都要出凸包队列,小心long long的乘法
while (hh < tt && (__int128)(f[q[tt]] - f[q[tt - 1]]) * (sc[i] - sc[q[tt - 1]]) >= (__int128)(f[i] - f[q[tt - 1]]) * (sc[q[tt]] - sc[q[tt - 1]]))
tt--;
q[++tt] = i;
}
printf("%lld\n", f[n]);
return 0;
}