[NOIP 2011 提高组] 观光公交

GitHub跳转原题关系图返回列表

每次贪心选择当前能让最多乘客少 1 分钟的那段路,把它的行驶时间减 1;这次收益由提前效果能传播到的连续区间内下车人数决定。

OJ: luogu

题目 ID: P1315

难度:提高+/省选-

标签:贪心模拟推导前缀和

日期: 2026-06-20 19:58

题意

公交车从 1 号景点依次开到 n 号景点。

i 段路原本耗时 D_i。 在每一站出发前,公交车都必须等这一站所有要上车的乘客到齐。

现在有 k 个加速器,每用一次可以让某一段 D_i 减少 1,但不能减成负数。

要求把所有乘客旅行时间总和降到最小。

思路

先看一个小数据下最直观的暴力版本:

cpp
#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 站。

接下来分两种情况:

  1. 如果第 i+1 站原本不需要等乘客,那么它也会提前 1 分钟出发,影响继续向后传;
  2. 如果第 i+1 站原本就要等乘客,那这 1 分钟提前会被等人的时间抵消,影响停止。

所以一次减 1 的影响范围,一定是从 i+1 开始的一段连续区间。

再看收益。

如果某个站的到达时间提前了 1 分钟, 那么所有在这个站下车的乘客,旅行时间都会减少 1

因此,把 D_i 减少 1 的收益,正好就是:

text
受影响连续区间内下车乘客数之和

于是可以贪心:

  • 每次都选当前收益最大的那一段路去减 1

实现上,先重建当前时刻表:

text
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]

这样它的收益就是:

text
prefix_down[stop_end[i]] - prefix_down[i]

其中 prefix_down 是“每站下车人数”的前缀和。

代码

cpp
#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;
}

复杂度

每次使用一个加速器,需要 O(n)O(n) 重建时刻表并 O(n)O(n) 扫一遍所有路段收益。

因此总时间复杂度是 O(kn)O(kn),空间复杂度是 O(n+m)O(n+m)

总结

这题表面上像“多次分配资源”,真正的关键是看出:

  • 一次加速器只会让后面一段连续区间整体提前 1
  • 它的收益就是这段区间内有多少乘客下车

看清这一点以后,贪心选择当前最大收益就很自然了。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析