[CSP-J 2019] 公交换乘

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

用全局时间队列清理过期优惠票,再按票价分桶寻找最早可用且票价足够的地铁券。

OJ: luogu

题目 ID: P5661

难度:普及-

标签:模拟队列cspj

创建: 2026-06-18 14:23

更新: 2026-10-25 19:18

题意

每次乘地铁都能得到一张优惠票:

  • 有效期 45 分钟;
  • 只能免费坐一次公交;
  • 只能用于票价不超过这次地铁票价的公交。

处理公交时:

  • 如果有可用优惠票,就一定要使用;
  • 如果有多张可用票,要优先使用获得时间最早的那张。

现在按时间顺序给出所有出行记录,要求求出总花费。

思路

先看最直接的做法:把每次地铁得到的优惠票都存起来。

遇到公交时,从最早的优惠票开始往后扫,找第一张满足:

  1. 还没被用过;
  2. 没过期;
  3. 地铁票价不少于当前公交票价。

找到就把它用掉,否则自己付公交钱。

这个写法最好理解:

cpp
#include <bits/stdc++.h>
using namespace std;

const int maxn = 100000 + 5;

struct Ticket {
    int price;
    int time;
    bool used;
};

int n;
int typ, price, tim;
long long ans;
vector<Ticket> tickets;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;

    for (int i = 1; i <= n; i++) {
        cin >> typ >> price >> tim;

        if (typ == 0) {
            ans += price;
            tickets.push_back({price, tim, false});
            continue;
        }

        int choose = -1;
        for (int j = 0; j < (int) tickets.size(); j++) {
            if (tickets[j].used) {
                continue;
            }
            if (tim - tickets[j].time > 45) {
                continue;
            }
            if (tickets[j].price < price) {
                continue;
            }
            choose = j;
            break;
        }

        if (choose == -1) {
            ans += price;
        } else {
            tickets[choose].used = true;
        }
    }

    cout << ans << '\n';
    return 0;
}

但它在每次公交时都可能扫描很多历史地铁票,最坏会变成平方级。

这题真正有用的观察有两个:

  1. 时间是递增输入的,所以过期券一定能按顺序从前面整段删掉。
  2. price_i <= 1000,票价范围很小,可以直接按票价开桶。

于是我们维护:

  • 一个全局时间队列,专门负责清理过期优惠票;
  • 1000 个价格桶队列,bucket[p] 里存票价恰好为 p 的地铁券编号;
  • 一个 removed 标记,表示某张券是否已经过期或被使用。

处理公交票价为 x 的记录时:

  • 只需看所有 p >= x 的桶;
  • 每个桶的队头都是这个票价下最早可用的券;
  • 再从这些候选里选编号最小的那张,就等价于“选获得时间最早的那张券”。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int maxn = 100000 + 5;   // 最大记录数 n <= 1e5
const int maxp = 1000 + 5;     // 票价上限 price <= 1000,按票价分桶

// 一张地铁优惠票的完整信息
struct Ticket {
    int price;  // 地铁票价(可抵扣的额度)
    int time;   // 乘地铁的时间
    int id;     // 记录编号(同时是获得顺序,越小越早)
};

int n;
int typ[maxn], price[maxn], tim[maxn];  // 第 i 条记录:类型(0地铁/1公交)、票价、时间
long long ans;                          // 总花费

// all_tickets:所有"未过期且未被使用"的地铁票,按获得时间(记录编号)递增
// 用它来按时间顺序丢弃超过 45 分钟的过期票
queue<Ticket> all_tickets;

// bucket[p]:票价为 p 的地铁票的记录编号,同桶内编号递增
// 因为"若有多张可用票优先用最早的",所以每个桶只需看队首
queue<int> bucket[maxp];

// removed_ticket[i]:第 i 条记录(地铁票)是否已失效(过期/已使用)
// 采用"惰性删除":不在队列中间真正删除,只在出队时跳过
bool removed_ticket[maxn];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;

    for (int i = 1; i <= n; i++) {
        cin >> typ[i] >> price[i] >> tim[i];

        // 记录按时间递增给出,因此只需要从队首弹出过期的票
        // 条件:t_bus - t_subway > 45 即失效
        while (!all_tickets.empty() && all_tickets.front().time + 45 < tim[i]) {
            removed_ticket[all_tickets.front().id] = true;
            all_tickets.pop();
        }

        // 乘地铁:必须花钱,同时获得一张有效期为 45 分钟的优惠票
        if (typ[i] == 0) {
            ans += price[i];
            all_tickets.push({price[i], tim[i], i});
            bucket[price[i]].push(i);
            continue;
        }

        // 乘公交:在票价 >= 本次票价的桶里找最早获得的那张票
        int choose_id = -1;      // 选中的票的记录编号
        int choose_price = -1;   // 选中的票的票价(即所在桶)

        // 票价只需枚举到 1000;每个桶先惰性弹出已失效的票,再看队首
        for (int p = price[i]; p <= 1000; p++) {
            while (!bucket[p].empty() && removed_ticket[bucket[p].front()]) {
                bucket[p].pop();
            }

            if (!bucket[p].empty()) {
                int id = bucket[p].front();
                // 多个桶都有票时,取记录编号最小的(获得最早的)
                if (choose_id == -1 || id < choose_id) {
                    choose_id = id;
                    choose_price = p;
                }
            }
        }

        if (choose_id == -1) {
            // 没有可用优惠票,只能全价乘车
            ans += price[i];
        } else {
            // 用掉这张票:标记失效,并从对应桶中弹出
            removed_ticket[choose_id] = true;
            bucket[choose_price].pop();
        }
    }

    cout << ans << '\n';
    return 0;
}

复杂度

  • 时间复杂度:O(n∗1000)O(n * 1000)
  • 空间复杂度:O(n)O(n)

总结

这题难点不在模拟规则本身,而在把规则拆成两个容易维护的维度:

  • 时间是否过期;
  • 票价是否足够。

一旦利用了“票价最多只有 1000”这个条件,就能用非常朴素、但足够稳的分桶做法通过。