每次贪心选择当前能让最多乘客少 1 分钟的那段路,把它的行驶时间减 1;这次收益由提前效果能传播到的连续区间内下车人数决定。
OJ: luogu
题目 ID: P1315
难度:提高+/省选-
标签:贪心模拟推导前缀和
日期: 2026-06-20 19:58
题意
公交车从 1 号景点依次开到 n 号景点。
第 i 段路原本耗时 D_i。
在每一站出发前,公交车都必须等这一站所有要上车的乘客到齐。
现在有 k 个加速器,每用一次可以让某一段 D_i 减少 1,但不能减成负数。
要求把所有乘客旅行时间总和降到最小。
思路
先看一个小数据下最直观的暴力版本:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 15;
const int MAXM = 25;
int n, m, k;
int d[MAXN];
int remain_add[MAXN];
int latest_time[MAXN];
int from_station[MAXM];
int to_station[MAXM];
int start_time[MAXM];
long long answer = (1LL << 62);
// 按当前每段路减少后的时间,完整模拟一次公交过程。
long long simulate() {
int arrive_time[MAXN];
int depart_time[MAXN];
arrive_time[1] = 0;
for (int i = 1; i <= n; i++) {
depart_time[i] = max(arrive_time[i], latest_time[i]);
if (i < n) {
int cost = d[i] - remain_add[i];
arrive_time[i + 1] = depart_time[i] + cost;
}
}
long long total = 0;
for (int i = 1; i <= m; i++) {
total += 1LL * arrive_time[to_station[i]] - start_time[i];
}
return total;
}
// 枚举每一段路用了多少次加速器。
void dfs(int pos, int left_k) {
if (pos == n) {
answer = min(answer, simulate());
return;
}
int limit = min(left_k, d[pos]);
for (int use = 0; use <= limit; use++) {
remain_add[pos] = use;
dfs(pos + 1, left_k - use);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
for (int i = 1; i < n; i++) {
cin >> d[i];
}
for (int i = 1; i <= m; i++) {
cin >> start_time[i] >> from_station[i] >> to_station[i];
latest_time[from_station[i]] = max(latest_time[from_station[i]], start_time[i]);
}
dfs(1, k);
cout << answer << '\n';
return 0;
}brute.cpp 直接枚举每一段路到底减了多少次,再完整模拟公交时刻表。
这个做法能帮助理解题意,但正式数据显然不可能这样枚举。
关键在于想清楚:一次加速器到底产生什么效果。
如果把某一段 D_i 减少 1,
那么公交车从第 i 站出发后,会提前 1 分钟到达第 i+1 站。
接下来分两种情况:
- 如果第
i+1站原本不需要等乘客,那么它也会提前1分钟出发,影响继续向后传; - 如果第
i+1站原本就要等乘客,那这1分钟提前会被等人的时间抵消,影响停止。
所以一次减 1 的影响范围,一定是从 i+1 开始的一段连续区间。
再看收益。
如果某个站的到达时间提前了 1 分钟,
那么所有在这个站下车的乘客,旅行时间都会减少 1。
因此,把 D_i 减少 1 的收益,正好就是:
受影响连续区间内下车乘客数之和于是可以贪心:
- 每次都选当前收益最大的那一段路去减
1
实现上,先重建当前时刻表:
depart[i] = max(arrive[i], latest[i])
arrive[i+1] = depart[i] + D_i其中 latest[i] 表示第 i 站所有上车乘客中最晚到站的时间。
接着,对每一段 i 求:
- 把它减
1后,第i+1站会先提前1分钟 - 再看这
1分钟提前从第i+1站开始最远能传到哪一站stop_end[i]
这样它的收益就是:
prefix_down[stop_end[i]] - prefix_down[i]其中 prefix_down 是“每站下车人数”的前缀和。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
const int MAXM = 10005;
int n, m, k;
int d[MAXN];
int latest_time[MAXN]; // latest_time[i]:第 i 站最晚需要等到什么时候
int down_cnt[MAXN]; // down_cnt[i]:在第 i 站下车的人数
int prefix_down[MAXN]; // 下车人数前缀和
int arrive_time[MAXN]; // arrive_time[i]:到达第 i 站的时刻
int depart_time[MAXN]; // depart_time[i]:离开第 i 站的时刻
int stop_end[MAXN]; // 把第 i 段路减 1 后,影响最远能传到哪一站
int station_end[MAXN]; // 如果第 i 站到达时间提前 1,影响最远能传到哪一站
int from_station[MAXM];
int to_station[MAXM];
int start_time[MAXM];
// 按当前 d[i] 重建整条线路的到站/发车时刻表。
void rebuild_schedule() {
arrive_time[1] = 0;
for (int i = 1; i <= n; i++) {
depart_time[i] = max(arrive_time[i], latest_time[i]);
if (i < n) {
arrive_time[i + 1] = depart_time[i] + d[i];
}
}
}
// 计算当前总旅行时间。
long long calc_total_travel_time() {
long long total = 0;
for (int i = 1; i <= m; i++) {
total += 1LL * arrive_time[to_station[i]] - start_time[i];
}
return total;
}
// 预处理每一段路在“再减 1”时,影响最远能传播到哪一站。
void build_stop_end() {
station_end[n] = n;
for (int i = n - 1; i >= 2; i--) {
station_end[i] = i;
if (arrive_time[i] > latest_time[i]) {
station_end[i] = station_end[i + 1];
}
}
for (int i = 1; i < n; i++) {
stop_end[i] = station_end[i + 1];
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
for (int i = 1; i < n; i++) {
cin >> d[i];
}
for (int i = 1; i <= m; i++) {
cin >> start_time[i] >> from_station[i] >> to_station[i];
latest_time[from_station[i]] = max(latest_time[from_station[i]], start_time[i]);
down_cnt[to_station[i]]++;
}
for (int i = 1; i <= n; i++) {
prefix_down[i] = prefix_down[i - 1] + down_cnt[i];
}
while (k--) {
rebuild_schedule();
build_stop_end();
int best_pos = 0;
int best_gain = 0;
for (int i = 1; i < n; i++) {
if (d[i] == 0) {
continue;
}
int gain = prefix_down[stop_end[i]] - prefix_down[i];
if (gain > best_gain) {
best_gain = gain;
best_pos = i;
}
}
if (best_gain == 0) {
break;
}
d[best_pos]--;
}
rebuild_schedule();
cout << calc_total_travel_time() << '\n';
return 0;
}复杂度
每次使用一个加速器,需要
因此总时间复杂度是
总结
这题表面上像“多次分配资源”,真正的关键是看出:
- 一次加速器只会让后面一段连续区间整体提前
1 - 它的收益就是这段区间内有多少乘客下车
看清这一点以后,贪心选择当前最大收益就很自然了。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
