题解 P5017 [NOIP2018 普及组] 摆渡车
题意简述
[Link](P5017 [NOIP2018 普及组] 摆渡车)
这里给出我的抽象题意。
给定 \(n\) 个点,第 \(i\) 个点的坐标是 \(a_i\) ,现在要你选出 \(v\) 个点(\(v \geq 0\)),在保证任意两个选出的点的距离都 \(\geq m\) 的前提下,每一个给定的点到最近的一个在这个点右侧的你选出的点的距离和最小,你需要求出这个距离和。
\(1 \leq n \leq 5\times 10^2 ,1 \leq m\leq 10^2 ,0 \leq a_i \leq 4\times 10^6\)。
Solution
考虑 \(dp\) :设 \(f_i\) 表示只考虑所有在 \([1,i]\) 范围内的 \(a_j\) ,每一个 \(a_j\) 都存在一个选出的点在它的右边的情况下最小的距离和。
转移的话,有两种情况:
- \(1 \sim i\) 中只选 \(i\) 一个点,和为 \(\sum_\limits{a_j\leq i}i-a_j\)。
- 不存在一个 \(a_j=i\),那么 \(i\) 这个位置上可以不选,最小的和为 \(f_{i-1}\)。
- 对于所有情况都可以在 \(i\) 上选一个点,那么可以枚举上一个点在哪里,容易写出这一部分的状态转移方程:
设 \(t=\max_\limits{1\leq i\leq n}\{a_i\}\),因为保证选出的点两两距离 \(\geq m\) ,所以选择的最右边的点一定 \(\leq t+m-1\) (如果不是这样的话把这个点往左移一定不劣),所以答案为 \(f_{t+m-1}\)。
前两种转移都是非常好的,不需要优化,下面重点讨论第三种情况:
我们可以把求和拆开,用前缀和优化。设 \(cnt_i\) 表示有多少个 \(a_j\leq i\) ,\(sum_i\) 表示所有 \(a_j\leq i\) 的 \(a_j\) 的和,那么:
时间复杂度 \(\mathcal{O}(t^2)\) ,期望得分 \(50\) ,考虑优化。
注意到如果两个选出的点的距离 \(\geq 2m\) ,那么在它们中间再加上一个点,答案一定不会变劣。
也就是说,在状态转移方程中枚举的 \(j\) 到 \(i\) 的距离一定小于 \(2m\)(因为 \(j\) 是 \(1 \sim i\) 中选出的最后一个点,优化就如上一行所说的),那么状态转移方程可以变为:
同理,如果 \(i-m\sim i\) 根本就不存在一个给定的点(即 \(cnt_i=cnt_{i-m}\)),那么在 \(i-m\) 前面选点一定要比在 \(i-m+1\sim i\) 中选点更优,所以直接让 \(f_i=f_{i-m}\) 即可,不需要枚举。
有了这两个优化,可以发现,一个点最多让 \(m\) 个决策点需要枚举,总共有 \(n\) 个点,枚举一次的时间复杂度是 \(\mathcal{O}(m)\),那么总时间复杂度就是 \(\mathcal{O}(nm^2+t)\),足以通过。
代码如下:
#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
inline int read() {
int num = 0 ,f = 1; char c = getchar();
while (!isdigit(c)) f = c == '-' ? -1 : f ,c = getchar();
while (isdigit(c)) num = (num << 1) + (num << 3) + (c ^ 48) ,c = getchar();
return num * f;
}
const int N = 4e6 + 105 ,INF = 0x3f3f3f3f;
int sum[N] ,f[N] ,n ,m ,t ,cnt[N];
signed main() {
n = read() ,m = read();
for (int i = 1; i <= n; i++) {
int x = read();
sum[x] += x;
cnt[x]++;
t = max(t ,x);
}
t += m - 1;
for (int i = 1; i <= t; i++) sum[i] += sum[i - 1] ,cnt[i] += cnt[i - 1];
for (int i = 1; i <= t; i++) {
f[i] = cnt[i] * i - sum[i];
if (cnt[i] == cnt[i - 1]) f[i] = min(f[i] ,f[i - 1]);
if (i >= m && cnt[i] == cnt[i - m]) f[i] = min(f[i] ,f[i - m]);
else for (int j = max(i - 2 * m + 1 ,0); j <= i - m; j++)
f[i] = min(f[i] ,f[j] + (cnt[i] - cnt[j]) * i - (sum[i] - sum[j]));
}
printf("%d\n" ,f[t]);
return 0;
}