贪心算法:作业排序问题
Posted 中学生编程与信息学竞赛自学
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了贪心算法:作业排序问题相关的知识,希望对你有一定的参考价值。
本课程是从少年编程网转载的课程,目标是向中学生详细介绍计算机比赛涉及的编程语言,数据结构和算法。编程学习最好使用计算机,请登陆 www.3dian14.org (免费注册,免费学习)。
上一堂课程,我们学习了如何选择活动,使得在不发生冲突的前提下,能参与尽可能多的活动,这里没有考虑到不同的活动是否会带来不同的收益,假设它们的收益是一样的。
现在我们把问题修改下:
有一系列作业,每个作业必须在各自的截止时间点之前完成,并且已知完成每个工作需要花费的时间以及带来的收益。为了简单起见,假设完成每个工作都需要相等的单位时间,这样作业的截至时间点也就是单位时间的倍数。再假设一次只能安排一个作业,完成一个作业后才能继续下一个作业。问:如何选择作业并安排次序使它们能带来的总收益为最大?
我们用例子A来说明。
【输入】有4个作业a,b,c,d。它们各自的截止时间和能带来的收益如下:
【输出】请找出一种能带来最大收益的作业执行次序。
在例子中,下列作业次序能带来最大的收益:{c, a}
完成作业c,能带来的收益是40;因为做作业c,截止时间为1,它必须安排在0-1之间执行。这样已经错过了b或d的截止时间点(1),因此无法再选择b或d,只能选择作业a,a的截至时间点为4,来得及完成。做作业a的收益是20,因此完成作业c和a能带来的总收益是 40+20=60。而选择其他次序的作业,都无法超过收益60。
参见下面的动图:
我们再看一个例子B
【输入】有5个作业a,b,c,d,e。它们各自的截止时间和能带来的收益如下:
请你选择和安排能获取最大收益的作业次序,先不给出答案,请你先思考,如何解答,然后再继续阅读。
算法分析
一个简单粗暴的算法就是生成给定作业集合的所有子集,并检查每一个子集以确定该子集中作业的可行性,然后找出哪个可行子集可以产生最大收益。 这是一个典型的贪心算法。
算法思路如下:
1)根据收益递减顺序对所有作业进行排序。
2)将结果序列初始化,并把已排序作业中的第一个作业加入
3)依次按下面的规则处理剩余的n-1个作业
如果把当前作业加入结果序列,不会错过它的截止时间,那么把它加入结果序列;
否则忽略当前的作业。
该算法中最费时的操作是为作业找一个可用的作业空闲时间窗口,要保证完成尽可能多的高收益作业。可以采取的策略是我们为每个作业,遍历作业窗口并分配可用的最晚空闲时间窗口(当然必须在截止时间之前)。例如,假设作业J1的截止时间为t = 5,我们可以为此作业指定最晚的时间窗口(该时间窗口是空闲的,小于截止时间),就是4-5之间的空闲时间段。 之后另一个截止时间为5的作业J2再加入进来,为它分配的最晚时间窗口将会是3-4,因为已经将4-5分配了给作业J1。
那么为什么一定要为作业分配最晚的空闲时间窗口呢?而不是随意为它分配一个可用的时间窗口呢?
例如:假设作业J1的截止时间d1 = 5,产生效益40;作业J2的截止时间d2 = 1,产生效益20。假设对于作业J1,我们分配了0-1之间的时间窗,因此我们将在该时间窗口执行作业J1,导致现在无法执行作业J2(因为J1无法在截止时间d2=1之前完成了)。但是,如果为J1分配4-5之间的时间窗,则J2可以执行。因此为作业分配最晚的空闲时间窗口可以尽可能多地执行其他作业。
实例分析
我们采用该算法来分析例子B。
1)先按收益从大到小排序
2)把最大收益的作业a加入结果序列,得到{a},完成a需要1个单位时间,它的截止时间是2,因此可以为它安排的最晚时间窗口是1-2
3)接着看作业c, 它的截止时间也为2,不过此时1-2之间已经被a占用了,因此它最晚只能被安排在0-1之间执行。把c加入结果序列 ,得到{a,c},
4)接着看作业d, 它的截止时间为1,无法为它安排执行了,因为它唯一能执行的时间窗口0-1已经被作业c占据了
5)同样作业b, 它的截止时间为1,也不能加入结果序列
6) 最后看作业e, 它的截止时间为3,可以为它安排的最晚空闲执行时间段为2-3,可以加入结果序列,得到{a,c,e}
因此例子B的答案是 {a,c,e},您答对了吗?
请参考下面的动图:
算法实现
算法的C++代码实现如下:
#include<iostream>
#include<algorithm>
using namespace std;
// 采用结构类型表示作业
struct Job
{
char id; // 作业编号
int dead; // 截止时间
int profit; // 完成作业获取的收益
};
// 该函数用于按收益大小排序
bool comparison(Job a, Job b)
{
return (a.profit > b.profit);
}
// 按收益最大化选择作业并进行排序
void schedulingJob(Job arr[], int n)
{
// 按收益递减次序对作业队列排序
sort(arr, arr+n, comparison);
int result[n]; // 保存结果序列,记录对应的作业数组下标
bool slot[n]; // 记录对应时间窗口是否被占用
// 把所有时间窗口初始化为空闲状态
for (int i=0; i<n; i++)
slot[i] = false;
// 对排好序的作业队列依次执行
for (int i=0; i<n; i++)
{
//试图为当前作业找到可用的最晚的空闲时间窗口
//注意是从晚到早依次检查
for (int j=min(n, arr[i].dead)-1; j>=0; j--)
{
// 找到一个空闲时间窗口
if (slot[j]==false)
{
result[j] = i; // 把找到的作业加入结果序列
slot[j] = true; // 标记该时间窗口被占用
break;
}
}
}
// 输出结果
for (int i=0; i<n; i++)
if (slot[i])
cout << arr[result[i]].id << " ";
}
int main()
{
Job arr[] = { {'a', 2, 100}, {'b', 1, 19}, {'c', 2, 27},
{'d', 1, 25}, {'e', 3, 15}};
int n = sizeof(arr)/sizeof(arr[0]);
cout << "The maximum profit sequence of job:";
schedulingJob(arr, n);
return 0;
}
请点击阅读原文来观看动画交互课件
以上是关于贪心算法:作业排序问题的主要内容,如果未能解决你的问题,请参考以下文章