目录
一、问题概述
❤设有n个会议的集合C={1,2,…,n},其中每个会议都要求使用同一个资源(如会议室),而在同一时间内只能有一个会议使用该资源。每个会议i都有要求使用该资源的起始时间bi和结束时间ei,且bi < ei 。如果选择了会议i使用会议室,则它在半开区间[bi, ei)内占用该资源。如果[bi, ei)与[bj , ej)不相交,则称会议i与会议j是相容的。会场安排问题要求在所给的会议集合中选出最大的相容活动子集,也即尽可能地选择更多的会议来使用资源。
二、问题分析
★对该问题分析,我们可以列出三种不同的贪心策略:
我们可以通过列举一系列会议并将他们按照开始时间、持续时间、结束时间分别排序,并依据排好的顺序依次选择会议,找到可以选择尽可能多的会议的贪心策略。
☆最终,我们确定第三种贪心策略(以结束时间选择)可以选到更多的会议,满足要求。
三、例题演示(Java)
【例】这些会议按结束时间的升序排列如下表所示。设有10个会议等待安排,用贪心法找出满足目标要求的会议集合。
会议i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
开始时间bi | 1 | 3 | 2 | 5 | 3 | 5 | 6 | 8 | 8 | 2 |
结束时间ei | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
确定了贪心策略,这个题目就变得十分简单:
因为这道题目是已经按照结束时间排好序的,我们只需要通过ei <bj(上一个会议i的结束时间小于下一个会议j的开始时间)这个限制条件把符合要求的会议挑选出来就可以了。
以Java为例:
/*======Meeting类======*/
public class Meeting {
int i;
int Begin;
int End;
public Meeting() {
super();
}
public Meeting(int i, int begin, int end) {
super();
this.i = i;
Begin = begin;
End = end;
}
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
public int getBegin() {
return Begin;
}
public void setBegin(int begin) {
Begin = begin;
}
public int getEnd() {
return End;
}
public void setEnd(int end) {
End = end;
}
@Override
public String toString() {
return "Meeting [i=" + i + ", Begin=" + Begin + ", End=" + End + "]\n";
}
}
/*=====Main=====*/
import java.util.LinkedList;
import java.util.Vector;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
Meeting meet1 = new Meeting(1,1,4);
Meeting meet2 = new Meeting(2,3,5);
Meeting meet3 = new Meeting(3,2,6);
Meeting meet4 = new Meeting(4,5,7);
Meeting meet5 = new Meeting(5,3,8);
Meeting meet6 = new Meeting(6,5,9);
Meeting meet7 = new Meeting(7,6,10);
Meeting meet8 = new Meeting(8,8,11);
Meeting meet9 = new Meeting(9,8,12);
Meeting meet10 = new Meeting(10,2,13);
Vector<Meeting> v = new Vector<Meeting>();
v.add(meet1);
v.add(meet2);
v.add(meet3);
v.add(meet4);
v.add(meet5);
v.add(meet6);
v.add(meet7);
v.add(meet8);
v.add(meet9);
v.add(meet10);
System.out.println("==========全部会议如下============================");
System.out.println(v);
int e = 0;
LinkedList<Meeting> list = new LinkedList<Meeting>();
for(int i=0;i<v.size();i++) {
if(e<v.get(i).Begin) {
e=v.get(i).End;
list.add(v.get(i));
}
}
System.out.println("===========最优会议安排如下:=====================");
System.out.println(list);
}
}
我们将这十组数据储存在一个vector容器中,再遍历一次该容器,将符合条件ei <bj的挑选出来,输出就可以完成了。
输出结果如下:
==========全部会议如下============================
[Meeting [i=1, Begin=1, End=4]
, Meeting [i=2, Begin=3, End=5]
, Meeting [i=3, Begin=2, End=6]
, Meeting [i=4, Begin=5, End=7]
, Meeting [i=5, Begin=3, End=8]
, Meeting [i=6, Begin=5, End=9]
, Meeting [i=7, Begin=6, End=10]
, Meeting [i=8, Begin=8, End=11]
, Meeting [i=9, Begin=8, End=12]
, Meeting [i=10, Begin=2, End=13]
]
===========最优会议安排如下:=====================
[Meeting [i=1, Begin=1, End=4]
, Meeting [i=4, Begin=5, End=7]
, Meeting [i=8, Begin=8, End=11]
]
注:该题目已经按照结束时间排好序,对于会议结束时间非升序排列的题目,我们需要按照结束时间对其进行排序之后再进行挑选。
四、算法正确性证明
对贪心算法的正确性证明,我们要从两方面进行:
1.问题存在从贪心选择开始的最优解。(满足贪心选择性质)
2.一步一步的贪心选择能够得到问题的最优解。(满足最优子结构性质)
1.贪心选择性质
证明思路:(归纳法)先提出一个全局最优解,证明可以对该解加以修改(“剪枝”),将以贪心选择为基础的解加上,该解也为最优解。
会议i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
开始时间bi | 1 | 3 | 2 | 5 | 3 | 5 | 6 | 8 | 8 | 2 |
结束时间ei | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
依据该例题,我们从会议2开始选择(不选择会议1),我们可以选择{2,4,8}三个会议,得到一个全局最优解,这时,我们从这个解中去掉会议2,加上会议1,这个解仍然成立,故而可以证明存在从贪心选择开始的最优解。
2.最优子结构性质
证明思路:(反证法)现提出一个全局最优解,去掉其中的某一部分,剩下的解在局部也是最优的。
会议i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
开始时间bi | 1 | 3 | 2 | 5 | 3 | 5 | 6 | 8 | 8 | 2 |
结束时间ei | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
我们可以选择{1,4,8}这三个会议,得到一个全局的最优解,这时,我们把会议1从该解中去除,得到{4,8},只要证明{4,8}这个解是会议4~10部分(红色边框)的局部最优解,就可以证明其满足最优子结构性质。
证明过程:
假设A = {1,4,8}为全局的最优解,去掉第一个解,得到A' = {4,8},之后,我们再假设一个解A*比A'更优(解释:A'选择了两个会议,我们就假设A*在局部中选择了三个会议),我们就可以得出A' < A*,这时我们加上先前去掉的一部分,可以得出A'+{1} < A*+{1},而A'+{1}等价于我们之前假设的全局最优解A,即A < A*+{1},因此,如果在局部存在一个比A'更优的最优解,那么A就不是最优解,与我们最开始的假设相矛盾,所以A'必定是局部的最优解,故而证明其满足最优子结构性质。
五、其他版本
以上例题我们展示了Java版本代码,为辅助各位读者更好的学习,下面分别是C语言、C++以及Python实现上述算法。(语言的转换由ChatGPT完成,如发现错误,欢迎评论指正。)
C语言版本:
#include <stdio.h>
#include <stdlib.h>
// 会议结构体
struct Meeting {
int i;
int Begin;
int End;
};
int compare(const void *a, const void *b) {
return ((struct Meeting *)a)->End - ((struct Meeting *)b)->End;
}
int main() {
struct Meeting meetings[] = {
{1, 1, 4},
{2, 3, 5},
{3, 2, 6},
{4, 5, 7},
{5, 3, 8},
{6, 5, 9},
{7, 6, 10},
{8, 8, 11},
{9, 8, 12},
{10, 2, 13}
};
int n = sizeof(meetings) / sizeof(meetings[0]);
qsort(meetings, n, sizeof(meetings[0]), compare);
printf("==========全部会议如下============================\n");
for (int i = 0; i < n; i++) {
printf("Meeting [i=%d, Begin=%d, End=%d]\n", meetings[i].i, meetings[i].Begin, meetings[i].End);
}
int e = 0;
printf("===========最优会议安排如下:=====================\n");
for (int i = 0; i < n; i++) {
if (e < meetings[i].Begin) {
e = meetings[i].End;
printf("Meeting [i=%d, Begin=%d, End=%d]\n", meetings[i].i, meetings[i].Begin, meetings[i].End);
}
}
return 0;
}
C++版本:
#include <iostream>
#include <vector>
#include <algorithm>
// 会议类
class Meeting {
public:
int i;
int Begin;
int End;
Meeting(int i, int begin, int end) : i(i), Begin(begin), End(end) {}
};
bool compare(const Meeting &a, const Meeting &b) {
return a.End < b.End;
}
int main() {
std::vector<Meeting> meetings = {
{1, 1, 4},
{2, 3, 5},
{3, 2, 6},
{4, 5, 7},
{5, 3, 8},
{6, 5, 9},
{7, 6, 10},
{8, 8, 11},
{9, 8, 12},
{10, 2, 13}
};
std::sort(meetings.begin(), meetings.end(), compare);
std::cout << "==========全部会议如下============================" << std::endl;
for (const auto &meeting : meetings) {
std::cout << "Meeting [i=" << meeting.i << ", Begin=" << meeting.Begin << ", End=" << meeting.End << "]" << std::endl;
}
int e = 0;
std::cout << "===========最优会议安排如下:=====================" << std::endl;
for (const auto &meeting : meetings) {
if (e < meeting.Begin) {
e = meeting.End;
std::cout << "Meeting [i=" << meeting.i << ", Begin=" << meeting.Begin << ", End=" << meeting.End << "]" << std::endl;
}
}
return 0;
}
Python版本:
class Meeting:
def __init__(self, i, begin, end):
self.i = i
self.Begin = begin
self.End = end
meetings = [
Meeting(1, 1, 4),
Meeting(2, 3, 5),
Meeting(3, 2, 6),
Meeting(4, 5, 7),
Meeting(5, 3, 8),
Meeting(6, 5, 9),
Meeting(7, 6, 10),
Meeting(8, 8, 11),
Meeting(9, 8, 12),
Meeting(10, 2, 13)
]
meetings.sort(key=lambda x: x.End)
print("==========全部会议如下============================")
for meeting in meetings:
print(f"Meeting [i={meeting.i}, Begin={meeting.Begin}, End={meeting.End}]")
e = 0
print("===========最优会议安排如下:=====================")
for meeting in meetings:
if e < meeting.Begin:
e = meeting.End
print(f"Meeting [i={meeting.i}, Begin={meeting.Begin}, End={meeting.End}]")