You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

163 lines
6.7 KiB

2 years ago
## [$AcWing$ $475$. 摆渡车](https://www.acwing.com/problem/content/description/477/)
### 一、题目描述
有$n$名同学要乘坐摆渡车从 人大附中 前往 人民大学,第 $i$ 位同学在第 $t_i$分钟去等车。只有一辆摆渡车在工作,但摆渡车容量可以视为无限大。摆渡车从人大附中出发, 把车上的同学送到人民大学,再回到人大附中(去接其他同学),这样往返一趟总共花费$m$分钟(同学上下车时间忽略不计)。摆渡车要将所有同学都送到人民大学。
凯凯很好奇,如果他能任意安排摆渡车出发的时间,那么这些同学的 **等车时间之和最小** 为多少呢?
**注意**:摆渡车回到人大附中后可以即刻出发。
**输入格式**
第一行包含两个正整数 $n,m$,以一个空格分开,分别代表等车人数和摆渡车往返一趟的时间。
第二行包含 $n$ 个正整数,相邻两数之间以一个空格分隔,第 $i$ 个非负整数 $t_i$代表第$i$个同学到达车站的时刻。
**输出格式**
输出一行,一个整数,表示所有同学等车时间之和的最小值(单位:分钟)。
**输入样例**
```cpp {.line-numbers}
5 5
11 13 1 5 5
```
**输出样例**
```cpp {.line-numbers}
4
```
### 二、解题思路
如果把时间看作一个数轴,那么所有同学到达车站的时刻就是数轴上的点,安排车辆的工作就是把数轴划分成若干个 **左开右闭** 的子区间,如下图所示:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202310091646906.png)
例如测试样例中摆渡车安排在时刻$1、6、13$时,所有同学的等待时间之和最少,为$4$。一共安排了$3$次乘车,将数轴划分$3$段左开右闭的区间,分别是:$(0,1],(1,6],(6,13]$,每段长度都$≥m$,上图中$m=5$。
这样,等待时间之和就转换成了所有点到各自所属区间右边界的距离之和。
#### 状态表示
设$f[i]$表示数轴上对于前$i$个点,且最后一段的右边界为$i$,位于$(0,i]$的所有点到各自所属区间右边界的 **距离之和的最小值**。
#### 状态转移
设最后一段是$(j,i]$,而每段长度$ij≥m$,则有$j≤im$。如果第$k$位同学到达时间$t[k]$属于区间$(j,i]$,即$j<t[k]i$$i$$it[k]$$(j,i]$$i$$f[j]$
$$\large f[i]=\min_{j≤im}(\sum_{j<t[k]≤i}(it[k])+f[j])$$
进一步思考
$$\sum_{j<t[k]≤i}(it[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≤im}(cnt[i]cnt[j])×i(sum[i]sum[j])+f[j]$$
#### 时间复杂度
$f[i]$表示的是在时间轴上对于前$i$个点的最优解,$i≤4×10^6$,所以时间复杂度为$O(10^{12})$。
#### 代码实现
```cpp {.line-numbers}
#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_{i2m<jim}(cnt[i]cnt[j])×i(sum[i]sum[j])+f[j]$$
- **剪去无用状态**
对于某个阶段的状态$f[i]$,如果在区间$(im,i]$中没有任何点,那么状态$f[i]=f[i-m]$。因为区间中没有任何学生,可以不安排摆渡车,不会增加学生的等待时间。
优化后时间复杂度$O(nm^2+t)$。
#### 代码实现
```cpp {.line-numbers}
#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]);
}
// 注意,最后一名同学上车的时刻在[TT + m),求 其中最小值。
for (int i = T; i < T + m; i++) ans = min(ans, f[i]);
cout << ans << endl;
return 0;
}
```