贪心算法之活动分配问题
在此之前,我们还讨论过贪心算法的活动选择问题,活动选择问题里面的选择策略在这篇文章里面作为贪心选择策略用到。好吧,让我们进入主题。
问题描述
有一个活动集合 S={a1,a2,a3,...an} ,每一个活动 ai 都有一个开始时间 si 和结束时间 fi ,那么活动 ai 占用的时间段为 [si,fi) 。如果活动 ai 和 aj 的时间段没有交集重叠,那么这两个活动是兼容的,即满足 si≤fj 或者 fi≥sj , [ai , aj] 就是兼容的。现在我们需要为这些活动安排教室,保证活动之间各不冲突。请问怎么安排才能保证使用的教室数量最少呢?
问题分析
我们的目标是将全部活动用最少的资源进行分配,我们现在有两种思路:
思路一:
贪心策略就是要做出一个选择,使得选择以后剩下的资源最多。那我们要怎么在这些活动里面选择呢?我们不妨在活动 S 里面选出一组活动 Si , Si 里面活动与活动彼此兼容,只要我们选出这样一组尽可能多的包含兼容活动的集合放入同一个教室,即选出 S 的一个最大兼容子集放入同一个教室。以此类推,对于剩下的活动也采用这种策略,直到活动为空
下面我们给出这个思路的流程图:
Created with Raphaël 2.1.2 start Erase the maximum compatible subset add a new room The remaining activities is empty? return room count end yes no
我们知道最大优先子集的运行时间为 O(n) ,每次都要选出一个最大兼容子集,运行时间为 O(n) ,所以该算法运行时间为 O(n2)
但是很遗憾,这种贪心策略不一定可以得到最优解,下面我们介绍第二种思路
思路二:
我们知道每一个活动都有一个开始时间和结束时间,我们现在只关心活动的开始和活动的结束,活动开始,我们就要为活动分配一个房间,活动结束,我们就要归还一个房间。我们可以创建两个房间的集合freeSet,busySet,一开始房间的集合房间数量为0(freeSet = 0 , busySet = 0)。
要开始一个活动时,需要一个房间,我们首先检查freeSet里面有没有房间
- 如果freeSet = 0,那么说明没有房间,我们需要向总部申请一个房间,就让busySet++
- 如果freeSet > 0 ,那么说明我们有房间,那么我们不要向总部申请房间,直接从freeSet里面得到一个房间freeSet–,busySet++
那么这个思路的关键就是检查活动什么时候开始,什么时候结束,我们将所有时间(开始时间和结束时间)进行升序排序得到时间序列 T ,遍历 T ,要是 t 是开始时间,那么就申请一个房间,若是结束时间,那么就规划一个房间
下面是思路二的流程图:
Created with Raphaël 2.1.2 start get time t is End time? busySet– freeSet++ traverse all times? return room count end freeSet = 0? busySet++ freeSet– yes no yes no yes no
我们知道上面的算法首先要将时间进行排序,消耗为 O(nlogn) ,后面需要便利一次所有时间,消耗为 O(2n) ,于是该算法时间消耗为 O(nlogn)+O(2n) ,即为 O(nlogn)
代码实现
下面首先实现第二种思路的代码:
/************************************************* * @Filename: activityAllocate_v1.cc * @Author: qeesung * @Email: qeesung@qq.com * @DateTime: 2015-04-27 14:02:51 * @Version: 1.0 * @Description: 活动分配问题,采用贪心算法来求解 **************************************************/
#include <iostream>
#include <utility>
#include <vector>
#include <algorithm>
#include <stdexcept>
using namespace std;
/** * 定义时间的类型 */
enum TIME_T{
START_T,//开始时间类型
END_T//结束事件类型
};
/** 时间的排序谓词 */
bool timeCompare(pair<unsigned int , TIME_T> a ,\
pair<unsigned int , TIME_T> b)
{
return a.first < b.first;
}
/** * 得到活动的最少占用教室数量 * @param activities 活动链表 * @return 最小数量 */
unsigned int greatActivityAllocate(std::vector<pair<unsigned int , unsigned int> > & activities) throw(logic_error)
{
// 首先需要将活动里面的时间提取出来
std::vector<pair<unsigned int , TIME_T> > timeVec;
for (std::vector<pair<unsigned int , unsigned int> >::iterator iter = activities.begin();\
iter != activities.end(); ++iter)
{
if(iter->first > iter->second)
throw logic_error("end time should greater than start time");
timeVec.push_back(pair<unsigned int , TIME_T>(iter->first , START_T));
timeVec.push_back(pair<unsigned int , TIME_T>(iter->second , END_T));
}
// 按照first , 排序timeVec
stable_sort(timeVec.begin() , timeVec.end() , timeCompare);
// 遍历timeVec数组
unsigned int freeNum = 0;
unsigned int busyNum = 0;
for (std::vector<pair<unsigned int , TIME_T> >::iterator iter = timeVec.begin();\
iter != timeVec.end(); ++iter)
{
// 是开始时间,那么说明需要分配一间教室
// 那么现在需要从freeNum里面开始分配
if(iter->second == START_T)
{
// 空间教室数量为0,那么需要从资源里面新加一间教室
busyNum++;
if(freeNum != 0)
freeNum--;
}
else// 这里是结束时间
{
// 那么需要从忙的教室里面归还一间教室
busyNum--;
freeNum++;
}
}
// 到最后所有教室都会被归还,那么freeNum里面就是被使用过的教书总量
return freeNum;
}
int main(int argc, char const *argv[])
{
std::vector<pair<unsigned int , unsigned int> > activities;
activities.push_back(pair<unsigned , unsigned>(1,10));
activities.push_back(pair<unsigned , unsigned>(12,16));
activities.push_back(pair<unsigned , unsigned>(9,17));
activities.push_back(pair<unsigned , unsigned>(1,8));
try{
unsigned int classroomNum = greatActivityAllocate(activities);
cout<<"activities use at least "<<classroomNum<<" classroom(s)"<<endl;
}catch(logic_error e)
{
cerr<<e.what()<<endl;
}
return 0;
}
编译以后运行结果为:
activities use at least 2 classroom(s)
下面我们来看思路一的代码实现
/************************************************* * @Filename: activityAllocate_v2.cc * @Author: qeesung * @Email: qeesung@qq.com * @DateTime: 2015-04-27 14:36:36 * @Version: 1.0 * @Description: 活动分配问题,采用贪心算法来求解,每次选出最大兼容自己作为贪心策略 **************************************************/
/** * 这里我们首先采用贪心策略求出一个集合里面的最大兼容子集 * 然后将这个最大兼容子集共用一个教室 * 然后再求出剩下的活动的最大兼容子集,放入下一个教室 * 这样一直循环,直到没有活动为止 * 这里一共用到两次贪心策略 */
#include <iostream>
#include <utility>
#include <vector>
#include <set>
using namespace std;
/** * 选出最大活动兼容子集 * @param activities 活动列表 * @param selected 选出来的活动 * @param solution 这次有哪些活动被选出来 * @return 活动数量 */
unsigned int greateActivitySelector(\
std::vector<pair<unsigned int , unsigned int> > & activities,\
std::set<unsigned int> & selected,\
std::vector<unsigned int> & solution)
{
solution.clear();
/** 这里主要采用迭代的方式进行 */
/** 确认还有活动 */
if(activities.size() - selected.size() == 0)
return 0;
unsigned int count = 0;
unsigned int finishTime = 0;
unsigned int i = 0;
while(i < activities.size() && selected.count(i) != 0)// 选出没有被选中过的
++i;
if(i >= activities.size())
return 0;
solution.push_back(i);
selected.insert(i);
finishTime = activities[i].second;
++count;
for (; i < activities.size(); ++i)
{
// 找出没有选中的,而且是兼容的
while(i < activities.size())
{
if(selected.count(i) == 0)
{
if(activities[i].first > finishTime)
break;
}
++i;
}
if(i >= activities.size())
break;
// 表明已经找到,现在更新数据
solution.push_back(i);
selected.insert(i);
finishTime = activities[i].second;
++count;
}
return count;
}
/** * 计算分配教室的最优解 * @param activities 活动列表 * @return 最少教室数 */
unsigned int greatActivityAllocate(std::vector<pair<unsigned int , unsigned int> > & activities)
{
std::vector<unsigned int> solution;// 从来存储解决方案
std::set<unsigned int> selected;//用来存储活动时候被选择
unsigned int count = 0;
while(selected.size() < activities.size())
{
greateActivitySelector(activities , selected , solution);
cout<<"classroom"<<count<<":";
for (int i = 0; i < solution.size(); ++i)
{
cout<<"a"<<solution[i]<<"\t";
}
cout<<endl;
++count;
}
return count;
}
int main(int argc, char const *argv[])
{
std::vector<pair<unsigned int , unsigned int> > activities;
activities.push_back(pair<unsigned , unsigned>(1,8));
activities.push_back(pair<unsigned , unsigned>(1,10));
activities.push_back(pair<unsigned , unsigned>(12,16));
activities.push_back(pair<unsigned , unsigned>(9,17));
unsigned int classroomNum = greatActivityAllocate(activities);
cout<<"activities use at least "<<classroomNum<<" classroom(s)"<<endl;
while(1);
return 0;
}
classroom0:a0 a2
classroom1:a1
classroom2:a3
activities use at least 3 classroom(s)
发现这里的已经不是最优解了,这也就证实了贪心算法是不一定能得到最优解的,但是就算最后没有得到最优解,那也是次最优的!
下面有一个思路,博主觉得有点不好实现,我们通过思路一的启发,我们抛弃贪心算法,直接上贪心算法的大佬动态规划,每次都割去一个兼容子集,但不一定是最大兼容子集。
我们假设每次割去的子集数量为m,那么得到递归式子为:
C[s]=C[s−m]+1,其中m就是s的一个兼容子集
这么写有个难点,就是要知道
s 的所有兼容子集,想想都觉得麻烦,要是诸位有更好的方法,记得指点指点啊