题解 P5017 【摆渡车】
前言
听说今年普及组难度堪比提高 半退役提高选手,心血来潮,特地来接受这道经典好题的洗礼。
正文
这是一道题意简明的题,论解法却无比多样。我个人喜欢先把题意抽象化。
我们不妨认为时间是一条数轴,每名同学按照到达时刻分别对应数轴上可能重合的点。安排车辆的工作,等同于将数轴分成若干个左开右闭段,每段的长度
如果您无法理解复杂的文字描述,试图结合下面的这幅图思考:
这不就有个鲜明的线性
设最后一段对应
另外,特判
然而我们对
其中,
这里令
至此,我们有了
#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;
}
时间复杂度爆炸,怎么办?使用
这是原来的转移式:
实际上只需要:
不知你是否看到了区别?仍然考虑
时间复杂度降至
#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;
}
这样写毫无逼格,怎么办?使用
移除前两个优化,转变画风,
还是拆,拆,拆!
把只跟
这不是斜率优化裸题吗!斜率
每个状态点最多进出队列一次,时间复杂度
#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;
}
教练加强了这题,
掌握前两个优化的核心思想后,不难发现最优情况下,每个点到所属段右边界的距离
理论上,想要得到
总而言之,只要枚举一个
在同一段内的情况很简单,不用枚举
新开一段的情况,同样要保证段长
易知,随着
时间复杂度为
#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;
}
听同学说,他有一个
很抱歉,作者菜得可怜,把
尾注
或许您发现了,我给出的代码都特别短。是的,这道题考验的就是