1 条题解
-
0
思路与算法
设起点到山 的距离为 ,把猫的到达时刻变换为 。在时刻 出发的喂养员可接到它当且仅当 ,等待时间为 。允许负出发时刻,不要将负的 改成零。
将 非降排序,记前缀和 。把猫交给最早可接它的喂养员不会增加等待;按出发时刻排列人员后,各组可取为排序后的连续段。每段的最优出发时刻等于段内最大 。令 为前 只猫使用至多 人的最小等待,,其它为不可达,每层保留 以允许人员不用。枚举末组 :
$$f_k(i)=iA_i-S_i+\min_{0\le j<i}\{f_{k-1}(j)+S_j-jA_i\}.$$可达前驱生成斜率 、截距 的直线。斜率严格递减,查询点 非降,用队列维护最小值下凸包。对每个 先查询再加入 ,保证末组非空。两层滚动数组即可,层数截为 。
正确性说明
路程变换保持可接条件和等待时间。给定各人出发时刻,将每只猫交给最早可接它的人逐猫最优,排序后的分配形成连续区间,空组可删;各非空段最优出发时刻是其最大 ,所以连续分组包含一个最优解。
任一合法划分有最后一组,删除它后前缀须取最优,否则能改进;反之最优前缀加末组构成合法方案。因此递推既不遗漏也不低估。直线表达式与递推完全等价。斜率递减时,交点次序使中间线不再有优势区间就可删除;使用精确交叉乘积。查询点非降,若后线当前不劣于队首,以后也不劣,故队首可永久弹出。不可达前驱不入队,凸包始终保留一个最优前驱。归纳每层每个前缀求得最优值,最终输出 。
复杂度
令 。路程和前缀和初始化 ,排序 。每层每条线至多入队出队一次,查询共 ,总时间 。滚动数组、队列和排序数组 ,路程数组 ,总额外空间 ;数组初始化已计入时间。
易错点
- 原始到达时刻非负,变换值和出发时刻可以为负。
- 先查询后插入,限制 ,每层空前缀为零。
- 查询点可重复,队首以非严格比较弹出;斜率因下标不同而严格不同。
- 前缀、等待和直线值用 long long,交叉乘积可能超64位,先转 __int128 再乘。不可达哨兵不参与运算。
- 覆盖单猫、人员多于猫、重复变换值、最大规模与大正负变换值。测试为非官方训练数据。
参考代码(C++11)
#include <bits/stdc++.h> using namespace std; using ll = long long; struct Line { ll m, b; ll value(ll x) const { return m * x + b; } }; bool obsolete(Line a, Line b, Line c) { return (__int128)(b.b - a.b) * (b.m - c.m) >= (__int128)(c.b - b.b) * (a.m - b.m); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m, p; cin >> n >> m >> p; vector<ll> distance(n + 1); for (int i = 2; i <= n; ++i) { ll d; cin >> d; distance[i] = distance[i - 1] + d; } vector<ll> a(m + 1), prefix(m + 1); for (int i = 1, h; i <= m; ++i) { ll t; cin >> h >> t; a[i] = t - distance[h]; } sort(a.begin() + 1, a.end()); for (int i = 1; i <= m; ++i) prefix[i] = prefix[i - 1] + a[i]; p = min(p, m); const ll INF = 4000000000000000000LL; vector<ll> previous(m + 1, INF), current(m + 1); vector<Line> q(m + 1); previous[0] = 0; for (int layer = 1; layer <= p; ++layer) { int head = 0, tail = 1; q[0] = {0, 0}; current[0] = 0; for (int i = 1; i <= m; ++i) { while (tail - head >= 2 && q[head].value(a[i]) >= q[head + 1].value(a[i])) ++head; current[i] = a[i] * i - prefix[i] + q[head].value(a[i]); if (previous[i] < INF) { Line line = {-i, previous[i] + prefix[i]}; while (tail - head >= 2 && obsolete(q[tail - 2], q[tail - 1], line)) --tail; q[tail++] = line; } } previous.swap(current); } cout << previous[m] << '\n'; }
- 1