[CSP-S 2021] 廊桥分配

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

分别模拟国内和国际航班使用最小可用廊桥编号,再枚举两区廊桥数量分配。

OJ: luogu

题目 ID: P7913

难度:普及+/提高

标签:模拟贪心优先队列

日期: 2026-07-06 08:46

题意

机场一共有 nn 个廊桥,要把其中一部分分给国内区,剩下的分给国际区。国内航班只能用国内区廊桥,国际航班只能用国际区廊桥。

每个区内部遵循同一个规则:飞机按到达时间依次处理,如果此时有空闲廊桥,就停靠廊桥;否则只能停远机位。要求选择国内区廊桥数量,使停靠廊桥的飞机总数最大。

思路

最直接的想法是枚举国内区分到 ii 个廊桥,再分别模拟国内和国际航班。这个做法很好理解,适合小数据验证:

cpp
// brute.cpp:小数据暴力解,枚举国内区分到多少廊桥,再直接模拟先到先得。
#include <bits/stdc++.h>
using namespace std;

const int MAXM = 105;

struct Flight {
    int arrive;
    int leave;
};

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

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

int simulate(Flight flights[], int m, int 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 (int i = 1; i <= bridge_count; i++) {
        free_bridge.push(i);
    }

    int answer = 0;
    for (int 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 (int i = 1; i <= m1; i++) {
        cin >> domestic[i].arrive >> domestic[i].leave;
    }
    for (int i = 1; i <= m2; i++) {
        cin >> international_flight[i].arrive >> international_flight[i].leave;
    }

    int answer = 0;
    for (int domestic_bridge = 0; domestic_bridge <= n; domestic_bridge++) {
        Flight d[MAXM], g[MAXM];
        for (int i = 1; i <= m1; i++) {
            d[i] = domestic[i];
        }
        for (int i = 1; i <= m2; i++) {
            g[i] = international_flight[i];
        }
        int now = simulate(d, m1, domestic_bridge) + simulate(g, m2, n - domestic_bridge);
        answer = max(answer, now);
    }

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

暴力的瓶颈在于:每枚举一个 ii,都要重新模拟两类航班。如果 nn 和航班数都是 10510^5,这样会重复做大量相同工作。

关键观察是:对于同一个区域,我们可以一次模拟出“如果有 11 个、22 个、…、nn 个廊桥,各能接多少航班”。

模拟时,始终把飞机分配给编号最小的空闲廊桥。设某架飞机被分到编号 xx,这表示在它到达时,编号小于 xx 的廊桥都正在被占用;如果这个区域只有 x1x-1 个廊桥,它就无法停靠。因此,编号 xx 被使用的次数,可以看成“第 xx 个廊桥额外贡献的航班数”。

于是对每个区域做一次扫描:

  • 按到达时间排序所有航班;
  • 用一个小根堆维护已经离开的飞机,把它们占用的廊桥放回空闲集合;
  • 用另一个小根堆维护当前空闲廊桥编号;
  • 每次取最小空闲编号分配,并统计这个编号被使用了几次;
  • 最后做前缀和,得到 sum[i]sum[i]:给这个区域 ii 个廊桥时能接多少航班。

国内区得到 A[i]A[i],国际区得到 B[i]B[i] 后,枚举国内区分到 ii 个廊桥,答案就是:

text
max(A[i] + B[n - i])

代码

cpp
// main.cpp:用最小可用廊桥编号模拟每个区域,统计分配 i 个廊桥能接多少航班。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;

struct Flight {
    int arrive;
    int leave;
};

int n, m1, m2;
Flight domestic[MAXN], international_flight[MAXN];
int cnt_domestic[MAXN], cnt_international[MAXN];
int sum_domestic[MAXN], sum_international[MAXN];

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

void calc(Flight flights[], int 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;

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

    for (int 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));
        }
    }

    for (int 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 (int i = 1; i <= m1; i++) {
        cin >> domestic[i].arrive >> domestic[i].leave;
    }
    for (int 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);

    int ans = 0;
    for (int i = 0; i <= n; i++) {
        ans = max(ans, sum_domestic[i] + sum_international[n - i]);
    }
    cout << ans << '\n';

    return 0;
}

复杂度

设总航班数为 m1+m2m1 + m2

每个区域的航班排序复杂度为 O(mlogm)O(m log m),模拟时每架飞机最多进出堆一次,复杂度也是 O(mlogn)O(m log n)。最后枚举分配数量是 O(n)O(n)

总时间复杂度为 O((m1+m2)log(m1+m2)+n)O((m1 + m2) \log (m1 + m2) + n),空间复杂度为 O(n+m1+m2)O(n + m1 + m2)

总结

本题不要把“分配国内/国际廊桥数量”和“区内先到先得模拟”绑在一起重复做。先分别算出每个区域在不同廊桥数量下的收益,再做一次枚举合并,问题就变成两个收益数组的拼接。

核心实现点是:区内模拟必须使用最小可用廊桥编号,这样每个编号的使用次数才可以做前缀和,表示不同廊桥数量下的答案。