1. 问题背景与核心需求解析
机场廊桥作为连接航站楼与飞机的重要设施,其分配效率直接影响航班周转率。2021年CSP-S第三题"廊桥分配"正是基于这一现实场景设计的经典调度问题。题目要求我们在国际/国内两个区域分配有限廊桥资源,使得能够停靠的航班数量最大化。
这个问题的核心在于:当一架飞机抵达时,如果有空闲的廊桥则立即占用;如果没有可用资源则只能停靠远机位。我们需要找到一种分配方案(例如分配k个给国内,剩下的给国际),使得总共能够服务的航班数达到峰值。
2. 算法选择与数据结构设计
2.1 为什么选择堆结构
面对这类区间调度问题,堆(优先队列)展现出独特优势。具体到本题,我们需要:
- 实时追踪可用廊桥编号
- 快速获取最小可用编号(贪心选择)
- 在航班离开时及时回收廊桥
普通数组的查询效率是O(n),而小根堆可以在O(1)时间获取最小值,O(logn)完成插入删除,完美匹配需求。以下是典型操作示例:
cpp复制priority_queue<int, vector<int>, greater<int>> available; // 小根堆
available.push(1); available.push(2); // 初始化可用廊桥
int bridge = available.top(); // 获取最小可用编号
available.pop(); // 占用该廊桥
available.push(bridge); // 航班离开后回收
2.2 双堆维护策略
实际解题时需要维护两个堆:
- 可用堆:存储当前空闲的廊桥编号
- 占用堆:存储正在使用的廊桥及其释放时间(pair<时间,编号>)
当处理新航班时:
- 先检查占用堆中是否有已过期的记录
- 将这些廊桥回收至可用堆
- 从可用堆分配最小编号
这种设计保证了O(nlogn)的时间复杂度,其中n为航班数量。
3. 详细解题步骤拆解
3.1 输入预处理与排序
原始输入数据需要按到达时间排序,这是贪心算法正确性的前提。建议使用结构体存储航班信息:
cpp复制struct Flight {
int arrive, depart;
bool operator<(const Flight& f) const {
return arrive < f.arrive;
}
};
vector<Flight> domestic; // 国内航班
sort(domestic.begin(), domestic.end());
3.2 计算前缀最大值数组
对于每个可能的分配方案k(国内k个,国际m-k个),我们需要预先计算:
- pre_domestic[k]:国内区分配k个时的最大服务航班数
- pre_international[k]:国际区的类似数组
通过预处理这两个数组,最终答案就是max(pre_domestic[i] + pre_international[m-i])。
3.3 核心调度算法实现
以下是单区域的廊桥调度函数:
cpp复制vector<int> calculate(const vector<Flight>& flights, int total) {
priority_queue<int, vector<int>, greater<int>> available;
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> occupied;
for(int i=1; i<=total; ++i)
available.push(i);
vector<int> res(total+1, 0);
int cnt = 0;
for(const auto& f : flights) {
// 释放已离开的廊桥
while(!occupied.empty() && occupied.top().first < f.arrive) {
available.push(occupied.top().second);
occupied.pop();
}
if(!available.empty()) {
int bridge = available.top();
available.pop();
occupied.push({f.depart, bridge});
++cnt;
}
res[available.size()] = cnt; // 记录剩余k个时的服务数
}
return res;
}
4. 关键优化与边界处理
4.1 空间优化技巧
注意到廊桥编号只需要相对大小关系,可以用"虚拟编号"代替实际编号。当分配k个廊桥时,只需要保证使用的编号≤k即可,无需关心具体分配方案。
4.2 时间离散化处理
对于极端大的时间值(如1e9),可以先将所有时间点排序后映射到连续整数,避免使用大数组。这在CSP竞赛中虽不必要,但在工程实践中很有价值。
4.3 特殊测试用例分析
需要特别注意以下边界情况:
- 所有航班时间完全相同
- 航班时间完全不相交
- 单架飞机停留时间极长
- 廊桥数量为0或1的特殊情况
5. 完整代码框架与实现
cpp复制#include <bits/stdc++.h>
using namespace std;
struct Flight { int a, d; };
vector<int> process(const vector<Flight>& flights, int m) {
priority_queue<int, vector<int>, greater<int>> avail;
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> occupied;
for(int i=1; i<=m; ++i) avail.push(i);
vector<int> res(m+1);
int count = 0;
for(const auto& f : flights) {
while(!occupied.empty() && occupied.top().first < f.a) {
avail.push(occupied.top().second);
occupied.pop();
}
if(!avail.empty()) {
int br = avail.top();
avail.pop();
occupied.push({f.d, br});
++count;
}
res[avail.size()] = count;
}
return res;
}
int main() {
int n, m1, m2;
cin >> n >> m1 >> m2;
vector<Flight> dom(m1), intl(m2);
for(auto& f : dom) cin >> f.a >> f.d;
for(auto& f : intl) cin >> f.a >> f.d;
sort(dom.begin(), dom.end(), [](auto& x, auto& y){ return x.a < y.a; });
sort(intl.begin(), intl.end(), [](auto& x, auto& y){ return x.a < y.a; });
auto pre_dom = process(dom, n);
auto pre_int = process(intl, n);
int ans = 0;
for(int i=0; i<=n; ++i) {
ans = max(ans, pre_dom[i] + pre_int[n-i]);
}
cout << ans << endl;
return 0;
}
6. 复杂度分析与性能优化
6.1 时间复杂度分解
- 排序阶段:O(mlogm) 其中m为航班数
- 预处理阶段:O(mlogn) 每个航班最多入堆出堆一次
- 结果合并:O(n) 线性扫描
整体复杂度为O(mlogm + mlogn),完全满足题目约束(m≤1e5)。
6.2 内存使用优化
原始解法使用了两个优先队列和多个vector,实际可以进一步优化:
- 复用同一个优先队列
- 使用数组代替vector存储结果
- 采用更紧凑的结构体存储航班信息
7. 同类问题扩展与变式
7.1 多区域廊桥分配
当机场有超过两个区域(如A/B/C三区)时,问题变为多维背包问题,可能需要动态规划求解。
7.2 带权重的航班调度
如果不同航班有不同优先级(如紧急航班权重更高),可以扩展为带权区间调度,需要修改堆的比较函数。
7.3 动态廊桥数量
考虑廊桥可能临时维修的情况,问题会变得更加复杂,可能需要结合线段树等数据结构。
8. 实际工程中的应用思考
虽然本题来自算法竞赛,但其核心思想在以下场景有广泛应用:
- 云计算资源调度(VM分配)
- 共享单车停车位管理
- 会议室预约系统
- 医院床位调度
在这些场景中,我们同样需要高效管理有限资源,处理"到达-离开"事件,堆结构往往能提供最优解决方案。