贪心算法之会议安排问题
一、问题本质与核心目标
1. 问题定义
会议安排问题是经典的活动选择类优化问题,核心要素与目标如下:
- 输入条件:给定一组会议(每个会议包含「开始时间」「结束时间」和「会议编号」三个属性),所有会议共用一个会场。
- 核心规则:会场同一时间只能举办一个会议,即会议时间不能重叠。
- 优化目标:在满足时间不重叠的前提下,最大化会场可安排的会议数量。
2. 与分数背包问题的关键差异
对比维度 | 会议安排问题 | 分数背包问题 |
问题核心 | 时间资源的无重叠分配 | 容量资源的价值最大化利用 |
贪心策略依据 | 会议结束时间 | 物品单位价值 |
选择规则 | 会议不可分割,要么全安排要么不安排 | 物品可分割,支持部分装入 |
二、算法核心:贪心策略的原理与合理性
1. 贪心策略的核心逻辑
解决会议安排问题的贪心策略可概括为:优先选择「结束时间最早」的会议,若结束时间相同,则优先选择「开始时间最晚」的会议。
选择结束时间最早的会议,能最大限度地节省时间资源,为后续安排更多会议留出充足时间;而结束时间相同时选开始时间最晚的会议,可进一步减少对后续时间的占用,降低时间浪费。
2. 策略合理性证明
该策略能确保得到最优解,核心原因有两点:
- 早结束的会议能释放时间资源,让后续有更多机会安排其他会议,所以要避免因选择晚结束的会议导致大量时间被占用,而错失安排更多会议的可能。
- 当会议结束时间相同时,开始时间晚的会议占用时间更短(例如 A 会议 9:00 - 10:00,B 会议 9:30 - 10:00,B 会议开始时间更晚,所占用时间更短),能进一步优化时间利用效率,不会额外消耗过多时间资源。
三、数据结构设计与关键变量
1. 核心数据结构:会议结构体
通过结构体封装会议的三个关键属性,便于统一存储和处理:
struct Meeting {int start; // 会议开始时间int end; // 会议结束时间int id; // 会议编号
} meetings[10005]; // 存储所有会议的数组
2. 关键变量说明
- meetings[10005]:存储所有会议的数组,最大可容纳 10005 个会议,满足中等规模会议安排需求。
- n:用户输入的实际会议数量(需小于等于 10005)。
- count:统计可安排的会议总数,初始值为 1(默认先安排排序后第一个会议,即最早结束的会议)。
- lastEnd:记录上一个已安排会议的结束时间,初始值为排序后第一个会议的结束时间,用于判断后续会议是否可安排(后续会议开始时间需 ≥ 该值)。
四、核心算法实现
1. 第一步:会议排序函数
需先对所有会议按「结束时间升序」排序,结束时间相同时按「开始时间降序」排序,为后续贪心选择奠定基础,排序函数作为 sort 函数的比较器使用:
// 按结束时间升序排序,结束时间相同则按开始时间降序排序
bool cmp(const Meeting& a, const Meeting& b) {if (a.end == b.end) { // 结束时间相同的情况return a.start > b.start; // 开始时间大的排在前面}return a.end < b.end; // 结束时间不同时,结束时间小的排在前面
}
- 排序后,会议列表的优先级顺序为:结束时间从早到晚,结束时间相同则开始时间从晚到早,确保后续遍历能优先处理最利于节省时间的会议。
2. 第二步:贪心选择算法(maxMeetings 函数)
该函数是核心计算模块,实现会议的选择与数量统计,具体逻辑如下:
// 计算最大可安排会议数,参数 n 为会议总数量
int maxMeetings(int n) {if (n == 0) {// 边界条件:无会议时,可安排数量为 0return 0; }sort(meetings, meetings + n, cmp); // 调用排序函数,对会议进行排序int count = 1; // 初始化可安排会议数为 1,默认安排第一个会议int lastEnd = meetings[0].end; // 记录第一个会议的结束时间,作为初始参考值// 遍历排序后的会议数组for (int i = 1; i < n; ++i) {// 若当前会议开始时间≥上一个会议结束时间,说明时间不重叠,可安排if (meetings[i].start >= lastEnd) {++count; // 可安排会议数加 1lastEnd = meetings[i].end; // 更新上一个会议的结束时间为当前会议的结束时间}}return count; // 返回最大可安排会议数
}
五、完整流程与示例分析
1. 完整执行流程
整个问题的解决流程分为「输入 → 处理 → 输出」三阶段,逻辑清晰:
1. 输入阶段:
- 用户输入会议数量n(如输入 “4”,代表有 4 个会议)
- 依次输入每个会议的「开始时间」和「结束时间」(如输入 “8 10” “9 11” “10 12” “11 13”)
- 自动为每个会议分配编号(从 1 开始,第一个会议编号为 1,第二个为 2,以此类推)
2. 处理阶段:
- 调用 sort(meetings, meetings + n, cmp),按规则对会议排序。
- 调用 maxMeetings(n),计算最大可安排会议数。
3. 输出阶段
打印最终结果(如 “最多可安排的会议数量: 2”)。
2. 示例分步解析
以输入 “4;8 10;9 11;10 12;11 13” 为例,详细拆解处理过程:
- 步骤 1:排序后会议顺序:
会议 1(start = 8,end = 10,id = 1)→
会议 2(start = 9,end = 11,id = 2)→
会议 3(start = 10,end = 12,id = 3)→
会议 4(start = 11,end = 13,id = 4)
(按结束时间升序排序,结束时间均不同,无需按开始时间二次排序)。
- 步骤 2:依次判断并安排会议:
- 初始安排会议 1:count = 1,lastEnd = 10;
- 判断会议 2:start = 9 < lastEnd = 10,时间重叠,不安排;
- 判断会议 3:start = 10 ≥ lastEnd = 10,时间不重叠,安排,count = 2,lastEnd = 12;
- 判断会议 4:start = 11 < lastEnd = 12,时间重叠,不安排;
- 最终结果:可安排会议数为 2,与输出一致。
六、复杂度分析
1. 时间复杂度
整体时间复杂度由「排序操作」主导,具体如下:
- 排序操作:使用标准库sort函数,对n个会议排序,时间复杂度为O(n log n)
- 贪心选择遍历:从第二个会议开始,遍历 n - 1 个会议,时间复杂度为O(n)
- 总时间复杂度:O(n log n),适用于中等规模数据(如n≤10^5),处理效率较高
2. 空间复杂度
空间复杂度主要来自会议存储,为O(n):
- 仅需一个数组 meetings 存储 n 个会议的属性,无额外复杂数据结构,空间利用率高,不会造成过多内存占用。
七、适用场景与局限性
1. 适用场景
该算法适用于「单资源(如单个会场、单个设备)无重叠调度」且「追求资源利用率最大化(安排数量最多)」的场景,典型案例包括:
- 会场调度:单个会议室的会议安排,确保同一时间仅一个会议进行,最大化会议安排数量。
- 设备使用:单个打印机、投影仪等设备的使用调度,避免设备占用冲突,最大化设备使用次数。
- 个人日程规划:个人一天内的活动安排,确保活动时间不冲突,尽可能安排更多活动。
2. 局限性
该算法并非万能,存在以下适用边界:
- 不适用于「多资源调度」场景(如多个会场同时安排会议),此时需结合其他算法优化。
- 无法处理「会议有优先级」的场景,需在排序阶段增加优先级判断逻辑。
- 若存在「会议时间异常」的情况(如开始时间≥结束时间,如 “10 9”),需在输入阶段增加验证逻辑,避免排序和判断出错。