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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## [$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_{jim}(\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_{jim}(cnt[i]cnt[j])×i(sum[i]sum[j])+f[j]$$
#### 时间复杂度
$f[i]$表示的是在时间轴上对于前$i$个点的最优解,$i4×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<j≤im}(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;
}
```