1. 题目背景解析
UVa 12189 "Dinner Hall" 是国际大学生程序设计竞赛(ICPC)中的一道经典算法题目,主要考察选手对贪心算法和事件调度问题的理解和应用能力。题目场景设定在一个餐厅的就餐高峰期,需要合理安排顾客的进出时间以最大化座位利用率。
这道题最早出现在2009年的ICPC拉丁美洲区域赛中,属于中等难度级别的算法题。在实际编程竞赛训练中,它常被用作贪心算法和区间调度问题的典型教学案例。
2. 问题建模与分析
2.1 题目描述重述
题目给定N个顾客的到达时间(E)和离开时间(L),餐厅的座位容量为C。当某个时间点在场人数超过C时,就需要拒绝部分顾客进入。我们需要找到一个最优策略,使得被拒绝的顾客数量最少。
关键约束条件:
- 每个顾客的用餐时间至少为1单位时间
- 决策只能在顾客到达的瞬间做出(在线算法)
- 餐厅营业时间有限(通常题目会给出时间范围)
2.2 问题复杂度分析
这是一个典型的事件调度问题,属于计算几何中的区间调度范畴。如果我们把每个顾客的停留看作是一个时间区间[E, L],问题就转化为如何选择最多数量的不重叠区间。
对于在线算法版本(即必须即时做出决定),最优解的时间复杂度可以达到O(n log n),通过合理的排序和优先队列实现。
3. 核心算法设计
3.1 贪心算法选择
经过分析,这道题适合采用基于结束时间的贪心算法。具体步骤如下:
- 将所有顾客按照离开时间升序排序
- 维护当前餐厅中的顾客离开时间的最小堆
- 对于每个新顾客:
- 移除所有已经离开的顾客(离开时间 ≤ 当前顾客到达时间)
- 如果当前人数 < 容量C,直接接纳
- 否则,比较堆顶(最早离开)与新顾客的离开时间:
- 如果新顾客会更早离开,替换堆顶
- 否则拒绝新顾客
3.2 算法正确性证明
这个贪心选择的正确性基于以下观察:
- 让更早离开的顾客先使用座位,可以腾出位置给后来的顾客
- 当必须做出取舍时,保留能更快腾出座位的顾客是最优选择
- 该策略保证了在任何时刻,餐厅中的顾客都是能最快离开的组合
可以用交换论证法证明这个贪心选择的最优性:假设存在一个最优解与我们的贪心选择在某一步不同,我们可以通过交换使其一致而不减少接纳人数。
4. 实现细节与优化
4.1 数据结构选择
高效实现需要使用以下数据结构:
- 优先队列(最小堆):维护当前在餐厅的顾客的离开时间
- 事件队列:处理顾客到达和离开事件
C++实现示例:
cpp复制#include <queue>
#include <algorithm>
using namespace std;
struct Event {
int time;
bool isArrival;
// 其他必要字段...
};
int solve(vector<pair<int,int>>& customers, int C) {
sort(customers.begin(), customers.end(),
[](auto& a, auto& b) { return a.second < b.second; });
priority_queue<int, vector<int>, greater<int>> pq;
int rejected = 0;
for(auto& [E, L] : customers) {
while(!pq.empty() && pq.top() <= E) {
pq.pop();
}
if(pq.size() < C) {
pq.push(L);
} else if(L < pq.top()) {
pq.pop();
pq.push(L);
} else {
rejected++;
}
}
return rejected;
}
4.2 边界条件处理
需要特别注意的特殊情况:
- 多个顾客同时到达时的处理顺序
- 顾客到达和离开时间相同的情况
- 餐厅容量C=0或C≥N的极端情况
- 时间戳的表示范围(避免整数溢出)
5. 复杂度分析与优化
5.1 时间复杂度
- 排序阶段:O(n log n)
- 优先队列操作:每个元素最多入队出队各一次,O(n log n)
- 总体复杂度:O(n log n)
5.2 空间复杂度
- 存储顾客数据:O(n)
- 优先队列:最坏情况下O(n)
- 总体空间:O(n)
5.3 可能的优化方向
- 如果时间范围有限且离散,可以使用计数排序将排序复杂度降到O(n)
- 对于大规模数据,可以考虑多级优先队列或分块处理
- 并行化处理:将时间轴分段并行处理
6. 变种与扩展问题
6.1 离线算法版本
如果允许离线处理(提前知道所有顾客信息),可以设计更高效的算法:
- 扫描线算法:将时间轴离散化后扫描
- 最大流建模:转化为网络流问题
6.2 其他变种
- 带优先级的顾客(VIP等)
- 不同顾客占用座位数不同
- 餐厅座位随时间变化(如清洁时段)
- 考虑顾客等待队列
7. 实战注意事项
- 输入格式处理:UVa题目通常有严格的输入格式要求
- 浮点数比较:如果使用浮点时间戳,需要设置epsilon比较
- 多测试用例处理:注意重置全局变量
- 输出格式:特别是空格和换行符的要求
重要提示:在编程竞赛中,这类问题通常会设置严格的时限,因此即使算法复杂度相同,实现的常数优化也很重要。建议使用更快的IO方法(如C++的ios::sync_with_stdio(false))。
8. 测试用例设计
有效的测试用例应包含:
- 一般情况测试
- 边界情况测试(C=0, C=1, C≥N)
- 时间重叠测试
- 大规模随机测试
示例测试用例:
code复制Input:
3 3
1 5
2 4
3 6
Output:
0
解释:容量为3,可以接纳所有顾客。
9. 常见错误与调试
新手容易犯的错误:
- 错误的事件处理顺序(应先处理离开事件再处理到达)
- 忽略了用餐时间至少1单位的约束
- 优先队列的比较函数写反
- 没有处理同时发生的事件
调试技巧:
- 打印事件处理的时间线
- 可视化顾客的区间分布
- 对小规模测试用例手工模拟
10. 实际应用场景
这类算法在实际中有广泛应用:
- 酒店房间预订系统
- 会议室调度
- 停车位管理
- 云计算资源分配
- 医院手术室安排
理解这个算法有助于解决许多资源分配和调度问题。在工业界面试中,类似的问题也经常出现,如Google的会议室调度、Uber的司机分配等问题都可以用这种思路解决。