1. 问题背景与核心需求
这道来自CSP-S 2021的题目"廊桥分配"看似是一个简单的资源调度问题,但深入分析后会发现它巧妙地将堆(优先队列)的应用场景与实际问题相结合。题目描述的是机场廊桥分配的场景:有n架飞机需要停靠,机场有国内和国际两个区域的廊桥,每个区域分配的廊桥数量会影响飞机的停靠效率。我们需要找到最优的廊桥分配方案,使得能够停靠的飞机总数最大化。
在实际机场运营中,廊桥属于高价值资源。一个廊桥在同一时间段只能供一架飞机使用,而远机位(需要摆渡车)的运营成本和时间消耗都显著更高。因此,这道题目具有现实意义——它本质上是在解决"如何用有限资源服务最多需求"的经典优化问题。
2. 解题思路与算法选择
2.1 暴力解法的局限性
最直观的想法是枚举所有可能的分配方案:将廊桥总数m分成国内和国际两部分(分配i个给国内,m-i个给国际),然后分别计算每种分配下能停靠的飞机总数。这种方法的时间复杂度是O(m*n^2),当n和m较大时(题目中n,m≤1e5),这种解法显然无法在合理时间内完成。
2.2 关键观察与优化思路
这道题的突破口在于认识到:当给某个区域分配k个廊桥时,能够停靠的飞机数量实际上是关于k的非递减函数。也就是说,增加廊桥数量不会减少可停靠的飞机数。基于这个单调性,我们可以:
- 预先计算对于国内/国际区域,分配1-k个廊桥时分别能停靠多少飞机
- 然后枚举所有可能的分配方案i(国内)+j(国际)=m,取最大值
这种预处理的思想将问题分解为两个独立的部分,大大降低了计算复杂度。
2.3 堆(优先队列)的应用场景
计算"给定k个廊桥能停靠多少飞机"的核心在于模拟飞机停靠的过程。这正是堆数据结构的典型应用场景:
- 将飞机按照到达时间排序
- 维护一个最小堆,记录当前可用廊桥的释放时间
- 对于每架飞机,检查是否有廊桥在其到达时已经释放:
- 如果有,复用该廊桥(更新堆)
- 如果没有且还有剩余廊桥,分配新廊桥
- 否则无法停靠
使用堆可以高效地管理廊桥资源,每次操作的时间复杂度是O(log k),整体复杂度为O(n log n)。
3. 详细实现步骤
3.1 数据预处理
cpp复制struct Flight {
int arrive, depart;
};
vector<Flight> preprocess(vector<Flight>& flights) {
sort(flights.begin(), flights.end(), [](const Flight& a, const Flight& b) {
return a.arrive < b.arrive;
});
return flights;
}
3.2 计算分配方案
cpp复制vector<int> calculateCapacity(vector<Flight>& flights, int max_bridges) {
vector<int> res(max_bridges + 1, 0);
for (int k = 1; k <= max_bridges; ++k) {
priority_queue<int, vector<int>, greater<int>> pq; // 小顶堆,存储廊桥释放时间
int count = 0;
for (auto& f : flights) {
while (!pq.empty() && pq.top() <= f.arrive) {
pq.pop();
}
if (pq.size() < k) {
pq.push(f.depart);
count++;
}
}
res[k] = count;
}
return res;
}
3.3 整合求解
cpp复制int solve(int m, vector<Flight>& domestic, vector<Flight>& international) {
auto dom = preprocess(domestic);
auto inter = preprocess(international);
int max_dom = min(m, (int)dom.size());
int max_inter = min(m, (int)inter.size());
auto cap_dom = calculateCapacity(dom, max_dom);
auto cap_inter = calculateCapacity(inter, max_inter);
int res = 0;
for (int i = 0; i <= max_dom; ++i) {
int j = m - i;
if (j >= 0 && j <= max_inter) {
res = max(res, cap_dom[i] + cap_inter[j]);
}
}
return res;
}
4. 复杂度分析与优化
4.1 时间复杂度
- 排序:O(n log n)
- 计算容量:O(m n log k),其中k是廊桥数
- 整合求解:O(m)
当m和n同数量级时,整体复杂度为O(n^2 log n),对于1e5的数据仍然不够高效。
4.2 优化思路
注意到calculateCapacity中对每个k都重新计算,实际上可以利用k和k-1之间的关系进一步优化。当从k-1增加到k时,新增的廊桥只会影响之前因无可用廊桥而被拒绝的飞机。这种优化可以将复杂度降至O(n log n)。
5. 实现细节与注意事项
5.1 边界条件处理
- 飞机时间区间可能重叠或包含
- 当m=0时的特殊情况
- 国内或国际航班为空的情况
5.2 堆的选择
C++中:
priority_queue默认是大顶堆- 小顶堆需要指定比较器:
greater<int>
Python中:
heapq模块只实现小顶堆- 存入负数可以模拟大顶堆
5.3 实际编码技巧
- 飞机排序前可以先过滤掉明显不可能停靠的航班(如到达时间晚于所有廊桥释放时间)
- 在堆操作时,可以先检查堆顶元素,避免不必要的pop/push操作
- 预处理阶段可以同时计算国内和国际航班,利用多线程加速
6. 同类问题与扩展
堆在资源调度问题中的应用非常广泛,类似的问题包括:
- 会议室安排:给定n个会议的时间区间,问最少需要多少会议室
- 课程安排:如何安排课程使冲突最少
- 出租车调度:如何安排出租车接乘客使等待时间最短
这类问题的共同特点是都需要在时间线上管理有限的资源,堆提供了高效的管理手段。
7. 个人实现心得
在实际编码中,有几点经验值得分享:
- 输入数据量大的时候,使用快速IO方法(如C++的
ios::sync_with_stdio(false)) - 测试时要构造极端数据:如所有航班时间相同、航班完全包含等情况
- 在计算
cap_dom和cap_inter时,可以只计算到m为止,避免不必要的计算 - 使用
reserve预先分配vector空间可以提升性能
一个容易忽略的细节是飞机到达和离开时间可能相同(即停靠时间为0),这种情况下是否允许停靠需要根据题意仔细判断。在本题中,假设这种情况是允许的。