[CSP-S 2021] 廊桥分配

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

先分别模拟国内和国际航班在最小可用廊桥编号策略下的收益前缀和,再枚举两区廊桥数量分配取最大值。

OJ: luogu

题目 ID: P7913

难度:普及+/提高-

标签:模拟贪心优先队列

创建: 2026-07-06 08:46

更新: 2026-10-07 10:45

形式化题目

机场有 nn 个廊桥,需分配给国内区和国际区。国内航班只能用国内区廊桥,国际航班只能用国际区廊桥。每个区内部按到达时间先到先得:有空闲廊桥就停靠,否则停远机位。求使停靠廊桥的飞机总数最大的分配方案。

解法路线

题面给出了三个真实的分数档位:

档位 真实约束 做法 瓶颈
20%20\% n⩽100, m1+m2⩽100n \leqslant 100,\ m_1+m_2 \leqslant 100 枚举分配再逐区模拟 n⋅mlog⁡mn \cdot m \log m
40%40\% n⩽5000, m1+m2⩽5000n \leqslant 5000,\ m_1+m_2 \leqslant 5000 收益前缀和 + 枚举分配 无
100%100\% n⩽105, m1+m2⩽105n \leqslant 10^5,\ m_1+m_2 \leqslant 10^5 同上 无

20%20\% 用暴力枚举,40%40\% 引入收益前缀和消除重复模拟,100%100\% 沿用同一算法、仅扩大数组规模。每层代码都能独立提交、逐层拿分。

暴力解法:n⩽100n \leqslant 100

适用范围

20%20\% 的数据满足 n⩽100, m1+m2⩽100n \leqslant 100,\ m_1+m_2 \leqslant 100,可以枚举所有分配方案再逐区模拟。

思路

枚举国内区分到 ii 个廊桥(0⩽i⩽n0 \leqslant i \leqslant n),分别模拟国内和国际航班的先到先得过程,取最大值。模拟时用最小堆维护空闲廊桥编号和正在占用的廊桥。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-01 22:39
 * update_at: 2026-10-01 22:39
 */
// subtask_20.cpp:20% 数据 n ≤ 100, m1+m2 ≤ 100,枚举国内分几个廊桥再模拟。
// 时间 O(n · m log m),空间 O(m)。覆盖 20% 档。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXM = 105;

// 航班:抵达时刻和离开时刻。
struct Flight {
    int arrive;
    int leave;
};

ll n, m1, m2;
Flight domestic[MAXM], international_flight[MAXM];

bool cmp_flight(const Flight &a, const Flight &b) {
    return a.arrive < b.arrive;
}

// 模拟一个区域分配 bridge_count 个廊桥,返回能接的航班数。
int simulate(Flight flights[], ll m, ll bridge_count) {
    if (bridge_count == 0) {
        return 0;
    }

    sort(flights + 1, flights + m + 1, cmp_flight);

    priority_queue<int, vector<int>, greater<int> > free_bridge;
    priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > busy;
    for (ll i = 1; i <= bridge_count; i++) {
        free_bridge.push((int)i);
    }

    int answer = 0;
    for (ll i = 1; i <= m; i++) {
        while (!busy.empty() && busy.top().first < flights[i].arrive) {
            free_bridge.push(busy.top().second);
            busy.pop();
        }

        if (!free_bridge.empty()) {
            int id = free_bridge.top();
            free_bridge.pop();
            answer++;
            busy.push(make_pair(flights[i].leave, id));
        }
    }
    return answer;
}

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

    cin >> n >> m1 >> m2;
    for (ll i = 1; i <= m1; i++) {
        cin >> domestic[i].arrive >> domestic[i].leave;
    }
    for (ll i = 1; i <= m2; i++) {
        cin >> international_flight[i].arrive >> international_flight[i].leave;
    }

    // 先排序一次,后续 simulate 不会改变顺序。
    sort(domestic + 1, domestic + m1 + 1, cmp_flight);
    sort(international_flight + 1, international_flight + m2 + 1, cmp_flight);

    // 枚举国内分几个廊桥,取两种分配的最大值。
    int answer = 0;
    for (ll domestic_bridge = 0; domestic_bridge <= n; domestic_bridge++) {
        int now = simulate(domestic, m1, domestic_bridge)
                + simulate(international_flight, m2, n - domestic_bridge);
        answer = max(answer, now);
    }

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

复杂度与瓶颈

时间 O(n⋅mlog⁡m)O(n \cdot m \log m)。n=100, m=100n = 100,\ m = 100 时很快,但 n=5000n = 5000 时就要做 50005000 次模拟,太慢。

子任务解法:n⩽5000n \leqslant 5000

适用范围

40%40\% 的数据满足 n⩽5000, m1+m2⩽5000n \leqslant 5000,\ m_1+m_2 \leqslant 5000。暴力的 nn 次重复模拟太慢,需要换成一次模拟 + 前缀和。

思路

关键观察:对同一个区域,可以一次模拟出"分配 ii 个廊桥能接多少航班"(1⩽i⩽n1 \leqslant i \leqslant n)。

模拟时始终把飞机分配给编号最小的空闲廊桥。设某架飞机被分到编号 xx,说明编号小于 xx 的廊桥都正被占用;若只有 x−1x-1 个廊桥,它就无法停靠。因此编号 xx 被使用的次数就是"第 xx 个廊桥额外贡献的航班数"。

对每个区域做一次扫描:按到达时间排序,用最小堆维护空闲廊桥编号和正在占用的廊桥,每次取最小空闲编号分配并统计使用次数,最后做前缀和得到 sum[i]sum[i]。

国内区得 A[i]A[i],国际区得 B[i]B[i],枚举国内分 ii 个廊桥,答案为 max⁡(A[i]+B[n−i])\max(A[i] + B[n-i])。

相比 20%20\% 暴力,这里不再对每个分配重新模拟,而是复用一次模拟的前缀和结果。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-01 23:21
 * update_at: 2026-10-01 23:21
 */
// subtask_40.cpp:40% 数据 n ≤ 5000, m1+m2 ≤ 5000,用收益前缀和 + 枚举分配。
// 核心思路:对每个区域一次模拟出"分配 i 个廊桥能接多少航班"的前缀和,再枚举分配取最大值。
// 相比 20% 暴力,不再对每个分配重新模拟,而是复用前缀和,时间降到 O(m log m + n)。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXN = 5005;

// 航班:抵达时刻和离开时刻(均 ≤ 10^8,int 足够)。
struct Flight {
    int arrive;
    int leave;
};

ll n, m1, m2;
Flight domestic[MAXN], international_flight[MAXN];
int cnt_domestic[MAXN], cnt_international[MAXN]; // 分配 i 个廊桥时新增接的航班数
int sum_domestic[MAXN], sum_international[MAXN]; // 前缀和:分配 ≤ i 个廊桥共接多少航班

bool cmp_flight(const Flight &a, const Flight &b) {
    return a.arrive < b.arrive;
}

// 模拟一个区域:分配最多 n 个廊桥,用最小可用编号策略,
// 统计每个廊桥编号各接了多少航班,最后做前缀和。
void calc(Flight flights[], ll m, int result[]) {
    sort(flights + 1, flights + m + 1, cmp_flight);

    // 最小堆:空闲廊桥编号(优先用编号最小的)。
    priority_queue<int, vector<int>, greater<int> > free_bridge;
    // 最小堆:(离开时刻, 廊桥编号),按离开时刻排序。
    priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > busy;

    ll limit = min(n, m);
    for (ll i = 1; i <= limit; i++) {
        free_bridge.push((int)i);
    }

    for (ll i = 1; i <= m; i++) {
        while (!busy.empty() && busy.top().first < flights[i].arrive) {
            free_bridge.push(busy.top().second);
            busy.pop();
        }

        if (!free_bridge.empty()) {
            int id = free_bridge.top();
            free_bridge.pop();
            result[id]++;
            busy.push(make_pair(flights[i].leave, id));
        }
    }

    // 前缀和:result[i] = 分配 ≤ i 个廊桥共接多少航班。
    for (ll i = 1; i <= n; i++) {
        result[i] += result[i - 1];
    }
}

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

    cin >> n >> m1 >> m2;
    for (ll i = 1; i <= m1; i++) {
        cin >> domestic[i].arrive >> domestic[i].leave;
    }
    for (ll i = 1; i <= m2; i++) {
        cin >> international_flight[i].arrive >> international_flight[i].leave;
    }

    calc(domestic, m1, sum_domestic);
    calc(international_flight, m2, sum_international);

    // 枚举国内分 i 个廊桥,国际分 n-i 个,取最大值。
    int ans = 0;
    for (ll i = 0; i <= n; i++) {
        ans = max(ans, sum_domestic[i] + sum_international[n - i]);
    }
    cout << ans << '\n';

    return 0;
}

复杂度与瓶颈

时间 O((m1+m2)log⁡(m1+m2)+n)O((m_1+m_2)\log(m_1+m_2) + n),空间 O(n+m1+m2)O(n + m_1 + m_2)。n=5000n = 5000 时轻松通过。100%100\% 档沿用同一算法,仅需把数组开到 10510^5。

正解

关键观察

与 40%40\% 档完全相同:一次模拟出每个区域的收益前缀和,再枚举分配。100%100\% 和 40%40\% 的算法完全相同,唯一区别是数据规模。

思路

  1. 对每个区域:按到达时间排序,用最小堆维护空闲廊桥编号和正在占用的廊桥,每次取最小空闲编号分配,统计每个编号被使用次数;
  2. 做前缀和,得到 sum[i]sum[i]:给这个区域 ii 个廊桥时能接多少航班;
  3. 国内区得 A[i]A[i],国际区得 B[i]B[i],枚举国内分 ii 个廊桥,答案为 max⁡(A[i]+B[n−i])\max(A[i] + B[n-i])。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-01 22:39
 * update_at: 2026-10-01 22:39
 */
// main.cpp:满分做法,用最小可用廊桥编号模拟每个区域,统计分配 i 个廊桥能接多少航班。
// 核心思路:先对每个区域独立算出"分配 i 个廊桥能接的航班数"前缀和,再枚举国内/国际分几个廊桥取最大值。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXN = 100005;

// 航班:抵达时刻和离开时刻(均 ≤ 10^8,int 足够)。
struct Flight {
    int arrive;
    int leave;
};

ll n, m1, m2; // 廊桥总数、国内航班数、国际航班数
Flight domestic[MAXN], international_flight[MAXN];
int cnt_domestic[MAXN], cnt_international[MAXN]; // 分配 i 个廊桥时新增接的航班数
int sum_domestic[MAXN], sum_international[MAXN]; // 前缀和:分配 ≤ i 个廊桥共接多少航班

// 按抵达时刻排序。
bool cmp_flight(const Flight &a, const Flight &b) {
    return a.arrive < b.arrive;
}

// 模拟一个区域:分配最多 n 个廊桥,用最小可用编号策略,
// 统计每个廊桥编号各接了多少航班,最后做前缀和。
void calc(Flight flights[], ll m, int result[]) {
    sort(flights + 1, flights + m + 1, cmp_flight);

    // 最小堆:空闲廊桥编号(优先用编号最小的)。
    priority_queue<int, vector<int>, greater<int> > free_bridge;
    // 最小堆:(离开时刻, 廊桥编号),按离开时刻排序。
    priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > busy;

    ll limit = min(n, m);
    for (ll i = 1; i <= limit; i++) {
        free_bridge.push((int)i);
    }

    for (ll i = 1; i <= m; i++) {
        // 先释放所有已离开的廊桥。
        while (!busy.empty() && busy.top().first < flights[i].arrive) {
            free_bridge.push(busy.top().second);
            busy.pop();
        }

        if (!free_bridge.empty()) {
            int id = free_bridge.top();
            free_bridge.pop();
            result[id]++;
            busy.push(make_pair(flights[i].leave, id));
        }
    }

    // 前缀和:result[i] = 分配 ≤ i 个廊桥共接多少航班。
    for (ll i = 1; i <= n; i++) {
        result[i] += result[i - 1];
    }
}

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

    cin >> n >> m1 >> m2;
    for (ll i = 1; i <= m1; i++) {
        cin >> domestic[i].arrive >> domestic[i].leave;
    }
    for (ll i = 1; i <= m2; i++) {
        cin >> international_flight[i].arrive >> international_flight[i].leave;
    }

    calc(domestic, m1, sum_domestic);
    calc(international_flight, m2, sum_international);

    // 枚举国内分 i 个廊桥,国际分 n-i 个,取最大值。
    int ans = 0;
    for (ll i = 0; i <= n; i++) {
        ans = max(ans, sum_domestic[i] + sum_international[n - i]);
    }
    cout << ans << '\n';

    return 0;
}

复杂度

  • 时间复杂度:O((m1+m2)log⁡(m1+m2)+n)O((m_1+m_2)\log(m_1+m_2) + n)。
  • 空间复杂度:O(n+m1+m2)O(n + m_1 + m_2)。

总结

本题的关键是把"分配廊桥数量"和"区内先到先得模拟"解耦:先分别算出每个区域在不同廊桥数量下的收益前缀和,再枚举合并。区内模拟必须使用最小可用廊桥编号,这样每个编号的使用次数才能做前缀和,表示不同廊桥数量下的答案。

三个分数档位串起来是一条"从重复模拟到前缀和"的路线:暴力枚举分配再逐区模拟 → 一次模拟 + 前缀和复用 → 扩大数组规模。真正需要记住的是"最小可用编号策略使得每个编号的使用次数可做前缀和"这一关键观察。

本文的 main.cpp 已用逐区模拟版 brute.cpp 进行 300 组随机对拍;subtask_40.cpp 也已对拍通过。