1 条题解
-
0
自动搬运
来自洛谷,原作者为

Sooke
做被自己雪藏的耳廓狐 | https://codeforces.com/profile/Sulfox搬运于
2025-08-24 22:06:14,当前版本为作者最后更新于2018-11-13 21:20:30,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
前言
听说今年普及组难度堪比提高 ,作为一名
半退役提高选手,心血来潮,特地来接受这道经典好题的洗礼。
正文
这是一道题意简明的题,论解法却无比多样。我个人喜欢先把题意抽象化。
我们不妨认为时间是一条数轴,每名同学按照到达时刻分别对应数轴上可能重合的点。安排车辆的工作,等同于将数轴分成若干个左开右闭段,每段的长度 。原本的等车时间之和,自然就转换成所有点到各自所属段右边界的距离之和。
如果您无法理解复杂的文字描述,试图结合下面的这幅图思考:

这不就有个鲜明的线性 模型了吗?设 表示对数轴上 分段,且最后一段的右边界是 ,位于 内的点到各自所属段右边界的距离之和最小值。转移式:
$$f_i = \min_{j \leqslant i-m}\{ f_j+ \sum_{j < t_k \leqslant i} i-t_k \} $$设最后一段对应 ,既然它的长度 ,则有 。我们知道 ,意味着第 个点在这段中,它到右端的距离 ,因而产生这么多的贡献。累加贡献,与 以前的最优答案 ,即可推出转移式。
另外,特判 单独作为一段的边界情况,即 。
然而我们对 束手无策,如何快速求出呢?这时前缀和有了重大用途。拆,拆,拆!
$$\sum_{j < t_k \leqslant i} i-t_k = (\sum_{j < t_k \leqslant i}i)-(\sum_{j < t_k \leqslant i}t_k)=(cnt_i - cnt_j) \times i - (sum_i - sum_j) $$其中, 表示 中点的个数, 表示 中点的位置之和。顺便改写下刚才的转移式:
$$f_i = \min_{j \leqslant i-m}\{ f_j + (cnt_i - cnt_j) \times i - (sum_i - sum_j)\} $$这里令 ,最终答案只需在 找最小的 即可。实际上, 包含了所有可能的答案。
至此,我们有了 分的优秀做法,时间复杂度为 。代码如下:
#include <cstdio> #include <algorithm> const int maxT = 4000105; int n, m, t, ti, ans = 1e9, cnt[maxT], sum[maxT], f[maxT]; int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &ti); t = std::max(t, ti); cnt[ti]++; sum[ti] += ti; } for (int i = 1; i < t + m; i++) { cnt[i] += cnt[i - 1]; sum[i] += sum[i - 1]; } // 前缀和. for (int i = 0; i < t + m; i++) { f[i] = cnt[i] * i - sum[i]; // 特判边界情况. for (int j = 0; j <= i - m; j++) { f[i] = std::min(f[i], f[j] + (cnt[i] - cnt[j]) * i - (sum[i] - sum[j])); } } for (int i = t; i < t + m; i++) { ans = std::min(ans, f[i]); } printf("%d\n", ans); return 0; }时间复杂度爆炸,怎么办?使用 优化法宝一:剪去无用转移!
这是原来的转移式:
$$f_i = \min_{j \leqslant i-m}\{ f_j + (cnt_i - cnt_j) \times i - (sum_i - sum_j)\} $$实际上只需要:
$$f_i = \min_{i - 2m < j \leqslant i-m}\{ f_j + (cnt_i - cnt_j) \times i - (sum_i - sum_j)\} $$不知你是否看到了区别?仍然考虑 段的长度,由于分的段数不会增大答案,当它的长度 时,我们完全可以再给它切一刀,得到不劣的答案。通过此性质,可剪去大量无用转移。
时间复杂度降至 ,按照写法常数可获得 ~ 不等的成绩,并不排除在 少爷机上超时的可能。
#include <cstdio> #include <algorithm> const int maxT = 4000105; int n, m, t, ti, ans = 1e9, cnt[maxT], sum[maxT], f[maxT]; int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &ti); t = std::max(t, ti); cnt[ti]++; sum[ti] += ti; } for (int i = 1; i < t + m; i++) { cnt[i] += cnt[i - 1]; sum[i] += sum[i - 1]; } // 前缀和. for (int i = 1; i < t + m; i++) { f[i] = cnt[i] * i - sum[i]; // 特判边界情况. for (int j = std::max(i - m - m + 1, 0)/*剪去无用转移*/; j <= i - m; j++) { f[i] = std::min(f[i], f[j] + (cnt[i] - cnt[j]) * i - (sum[i] - sum[j])); } } for (int i = t; i < t + m; i++) { ans = std::min(ans, f[i]); } printf("%d\n", ans); return 0; }不稳,很虚,怎么办?使用 优化法宝二:剪去无用状态!
举个例子,假设正在求 ,但在 中没有任何点,这个 相对来说就是 “无用” 的。原因是若最后一段长度恰好 ,这里面又没有任何点,不分割也罢。长度 时,完全可以把这一段的右边界往左“拖”,产生不劣的答案。
然而直接扔掉这个状态,会与上一个优化缩小转移范围起冲突,故无用的位置令 ,防止漏解。
可以证明**“有用”**的位置 个,故时间复杂度再次优化成 。期望得分 分。代码:
#include <cstdio> #include <algorithm> const int maxT = 4000105; int n, m, t, ti, ans = 1e9, cnt[maxT], sum[maxT], f[maxT]; int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &ti); t = std::max(t, ti); cnt[ti]++; sum[ti] += ti; } for (int i = 1; i < t + m; i++) { cnt[i] += cnt[i - 1]; sum[i] += sum[i - 1]; } // 前缀和. for (int i = 0; i < t + m; i++) { if (i >= m && cnt[i - m] == cnt[i]) { f[i] = f[i - m]; continue; } // 剪去无用状态. f[i] = cnt[i] * i - sum[i]; // 特判边界情况. for (int j = std::max(i - m - m + 1, 0)/*剪去无用转移*/; j <= i - m; j++) { f[i] = std::min(f[i], f[j] + (cnt[i] - cnt[j]) * i - (sum[i] - sum[j])); } } for (int i = t; i < t + m; i++) { ans = std::min(ans, f[i]); } printf("%d\n", ans); return 0; }这样写毫无逼格,怎么办?使用 优化法宝三:利用单调性质!
移除前两个优化,转变画风, 是 的决策点,满足:
$$f_i = f_j + (cnt_i - cnt_j) \times i - (sum_i - sum_j) $$还是拆,拆,拆!
$$f_i = f_j + cnt_i \times i - cnt_j \times i - sum_i + sum_j $$把只跟 有关的项移到左边,跟 有关的乘积放在中间,只跟 有关的项移到最右边:
$$\underline{f_j + sum_j}_{\ y} = \underline{i_{_{}}}_{\ k} \times \underline{cnt_j}_{\ x} + \underline{(f_i - cnt_i \times i + sum_i)}_{\ b} $$这不是斜率优化裸题吗!斜率 单调上升,维护下凸壳。对于 把 推入队列,即可保证决策点 。
每个状态点最多进出队列一次,时间复杂度 ,仍能拿到 分。
#include <cstdio> #include <algorithm> const int maxT = 4000105; int n, m, t, ti, ans = 1e9, l = 1, r, cnt[maxT], sum[maxT], q[maxT], f[maxT]; inline double getSlope(int u, int v) { return (double) (f[v] + sum[v] - f[u] - sum[u]) / (cnt[u] == cnt[v] ? 1e-9 : cnt[v] - cnt[u]); } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &ti); t = std::max(t, ti); cnt[ti]++; sum[ti] += ti; } for (int i = 1; i < t + m; i++) { cnt[i] += cnt[i - 1]; sum[i] += sum[i - 1]; } // 前缀和. for (int i = 0; i < t + m; i++) { if (i - m >= 0) { while (l < r && getSlope(q[r - 1], q[r]) >= getSlope(q[r], i - m)) { r--; } q[++r] = i - m; // 把可能成为最优解的推入队列. } while (l < r && getSlope(q[l], q[l + 1]) <= i) { l++; } // 把不可能成为最优解的弹出队列. f[i] = cnt[i] * i - sum[i]; // 特判边界情况. if (l <= r) { f[i] = std::min(f[i], f[q[l]] + (cnt[i] - cnt[q[l]]) * i - (sum[i] - sum[q[l]])); } // 斜率优化转移. } for (int i = t; i < t + m; i++) { ans = std::min(ans, f[i]); } printf("%d\n", ans); return 0; }教练加强了这题,,复杂度依赖 的做法都挂了,怎么办?
掌握前两个优化的核心思想后,不难发现最优情况下,每个点到所属段右边界的距离 。令 表示对 排序后(),第 个点距离所属段右边界 个单位时,第 个点距离之和的最小值。
理论上,想要得到 ,我们需要枚举 ,用 转移。可有趣的是,当前的第 个点,要么和第 个点在同一段内,要么抛弃第 个点,新开了一段,而自己是里面的第一个。
总而言之,只要枚举一个 ,用 转移得到 ,两者没有区别的。
在同一段内的情况很简单,不用枚举 就可以直接转移:
新开一段的情况,同样要保证段长 :
$$g_{i,\ j} = \min\limits_{t_{i-1} + k + m \leqslant t_i + j} \{ g_{i - 1,\ k} \} + j $$易知,随着 的增大,能够转移的 的上限也不断增大,故使用前缀最小值维护可以转移的 。
时间复杂度为 ,即 ,目前看应该是最快的 做法了。注意边界细节, 代码如下:
#include <cstdio> #include <algorithm> const int maxN = 505, maxM = 205; int n, m, mm, ans = 1e9, t[maxN], g[maxN][maxM]; int main() { scanf("%d%d", &n, &m); mm = m + m; for (int i = 1; i <= n; i++) { scanf("%d", &t[i]); } std::sort(t + 1, t + n + 1); // 排序. for (int i = 1; i <= n; i++) { g[i][0] = 1e9; // 先特判 j = 0 的情况. for (int j = 0; j <= std::min(t[i] - t[i - 1] - m, m - 1); j++) { g[i][0] = std::min(g[i][0], g[i - 1][j]); } for (int j = 1; j < mm; j++) { g[i][j] = std::min(g[i][j - 1], t[i] + j - t[i - 1] - m >= 0 && t[i] + j - t[i - 1] - m < mm ? g[i - 1][t[i] + j - t[i - 1] - m] : (int) 1e9); } // 前缀最大值维护新开一段的情况. for (int j = 0; j < mm; j++) { g[i][j] = std::min(g[i][j], t[i] + j - t[i - 1] < mm ? g[i - 1][t[i] + j - t[i - 1]] : (int) 1e9) + j; } // 分在同一段内的情况, 加上 j 的贡献. } for (int i = 0; i < m; i++) { ans = std::min(ans, g[n][i]); } printf("%d\n", ans); return 0; }听同学说,他有一个 的做法,怎么办?
很抱歉,作者菜得可怜,把 用到极致也只有 ,但不清楚贪心或乱搞是否能在 内解决本题。
尾注
或许您发现了,我给出的代码都特别短。是的,这道题考验的就是 的灵活运用,合理剪枝优化,利用好其性质,甚至能够用更加优美的做法暴踩标算。至于代码,只有会敲和懒得敲。
- 1
信息
- ID
- 4036
- 时间
- 2000ms
- 内存
- 256MiB
- 难度
- 5
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者