6.7 KiB
AcWing
475
. 摆渡车
一、题目描述
有n
名同学要乘坐摆渡车从 人大附中 前往 人民大学,第 i
位同学在第 t_i
分钟去等车。只有一辆摆渡车在工作,但摆渡车容量可以视为无限大。摆渡车从人大附中出发, 把车上的同学送到人民大学,再回到人大附中(去接其他同学),这样往返一趟总共花费m
分钟(同学上下车时间忽略不计)。摆渡车要将所有同学都送到人民大学。
凯凯很好奇,如果他能任意安排摆渡车出发的时间,那么这些同学的 等车时间之和最小 为多少呢?
注意:摆渡车回到人大附中后可以即刻出发。
输入格式
第一行包含两个正整数 n,m
,以一个空格分开,分别代表等车人数和摆渡车往返一趟的时间。
第二行包含 n
个正整数,相邻两数之间以一个空格分隔,第 i
个非负整数 t_i
代表第i
个同学到达车站的时刻。
输出格式 输出一行,一个整数,表示所有同学等车时间之和的最小值(单位:分钟)。
输入样例
5 5
11 13 1 5 5
输出样例
4
二、解题思路
如果把时间看作一个数轴,那么所有同学到达车站的时刻就是数轴上的点,安排车辆的工作就是把数轴划分成若干个 左开右闭 的子区间,如下图所示:
例如测试样例中摆渡车安排在时刻1、6、13
时,所有同学的等待时间之和最少,为4
。一共安排了3
次乘车,将数轴划分3
段左开右闭的区间,分别是:(0,1],(1,6],(6,13]
,每段长度都≥m
,上图中m=5
。
这样,等待时间之和就转换成了所有点到各自所属区间右边界的距离之和。
状态表示
设f[i]
表示数轴上对于前i
个点,且最后一段的右边界为i
,位于(0,i]
的所有点到各自所属区间右边界的 距离之和的最小值。
状态转移
设最后一段是(j,i]
,而每段长度i−j≥m
,则有j≤i−m
。如果第k
位同学到达时间t[k]
属于区间(j,i]
,即j<t[k]≤i
,他到右边界i
的距离是i−t[k]
。那么累加所有属于区间(j,i]
的点到i
点距离之和,然后加上上一阶段的状态f[j]
,就得到了状态转移方程:
\large f[i]=\min_{j≤i−m}(\sum_{j<t[k]≤i}(i−t[k])+f[j])
进一步思考
\sum_{j<t[k]≤i}(i−t[k])=(\sum_{j<t[k]≤i}{i})−(\sum_{j<t[k]≤i}{t[k]})
其中\displaystyle \sum{i}
和\displaystyle \sum{t[k]}
都可以通过前缀和 计算出来。
\displaystyle \sum{i}=(cnt[i]−cnt[j])×i
,cnt[i]
表示区间(0,i]
中同学的个数。
\displaystyle \sum{t[k]}=sum[i]−sum[j]
,sum[i]
表示区间(0,i]
中所有同学到达的时间之和。
状态转移方程可以写成:
\large f[i]=\min_{j≤i−m}(cnt[i]−cnt[j])×i−(sum[i]−sum[j])+f[j]
时间复杂度
f[i]
表示的是在时间轴上对于前i
个点的最优解,i≤4×10^6
,所以时间复杂度为O(10^{12})
。
代码实现
#include <bits/stdc++.h>
using namespace std;
// 通过了 10/20个数据
const int N = 4000010;
const int INF = 0x3f3f3f3f;
int f[N], cnt[N], sum[N];
int n, m; // 等车人数和摆渡车往返一趟的时间
int T; // T表示最后一个同学到达车站的时间
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int t; // 每个同学到达车站的时刻
cin >> t;
T = max(T, t); // 最后一个同学到达车站的时间
cnt[t]++; // t时刻引发的到达车站的人数,可能是多人
sum[t] += t; // t时刻引发的等待时间和,可能是多人
}
// 注意,这里应计算到最后一同学可能等到的时间
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] = i * cnt[i] - sum[i]; // 特殊处理i<m的情况
for (int j = 0; j <= i - m; j++)
f[i] = min(f[i], (cnt[i] - cnt[j]) * i - (sum[i] - sum[j]) + f[j]);
}
// 取右边界取的漂亮
int ans = INF;
for (int i = T; i < T + m; i++) ans = min(ans, f[i]);
cout << ans << endl;
return 0;
}
三、算法优化
DP
优化常用的两种方法:
- 剪去无用转移
考虑区间
(j,i]
,在计算f[i]
时,j
从0
开始枚举。其实可以缩小j
的范围,从i - 2 * m
开始枚举。因为当区间的长度≥2m
时,可以安排摆渡车跑一个来回,等待时间不会变长。通过此性质,可剪去大量无用转移,状态转移方程更新为:
\large f[i]=\min_{i−2m<j≤i−m}(cnt[i]−cnt[j])×i−(sum[i]−sum[j])+f[j]
- 剪去无用状态
对于某个阶段的状态
f[i]
,如果在区间(i−m,i]
中没有任何点,那么状态f[i]=f[i-m]
。因为区间中没有任何学生,可以不安排摆渡车,不会增加学生的等待时间。
优化后时间复杂度O(nm^2+t)
。
代码实现
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 4000010;
int ans = INF;
int cnt[N], sum[N], f[N];
int n, m; // 等车人数和摆渡车往返一趟的时间
int T; // T表示最后一个同学到达车站的时间
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int t;
cin >> t;
T = max(T, t);
cnt[t]++;
sum[t] += t;
}
// 注意,这里应计算到最后一同学可能等到的时间 T + m - 1
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++) {
// 可以多通过6个测试点
if (i >= m && cnt[i] == cnt[i - m]) {
f[i] = f[i - m];
continue;
}
f[i] = i * cnt[i] - sum[i]; // 特殊处理i<m的情况
// 可以多通过4个测试点
for (int j = max(0, i - 2 * m + 1); j <= i - m; j++)
f[i] = min(f[i], (cnt[i] - cnt[j]) * i - (sum[i] - sum[j]) + f[j]);
}
// 注意,最后一名同学上车的时刻在[T,T + m),求 其中最小值。
for (int i = T; i < T + m; i++) ans = min(ans, f[i]);
cout << ans << endl;
return 0;
}