在学习算法设计与分析的课程中,我发现贪心算法与其他算法相比具有一定的特殊性。特此将一些自己的想法和理解记录下来,供复习所用。
对贪心算法的思考
一想到贪心这个词语,人们总会有一种的不好的感觉,认为这个词的涵义不好,自带贬义。比如“贪心不足蛇吞象”,就是绝大多数人对“贪心”这个词的理解。但是,在编程层面,对研究算法的人来说,“贪心”并不是一个贬义词,下面三点就是我自己对”贪心“算法的认识。
- 人对美好事物的趋优性是与生俱来的。
- 追求美好生活和优质的东西,正是趋优性让我们生活美好、科技发展、人类进步。
- 凡事都有两面性,我们要积极看待“贪心”。
在理解到”贪心“的涵义之后,我们便可以开始认识贪心算法,即指从问题的初始解决方案开始,求解最优化问题包含一系列步骤,每一步都做好当前的最优选择,最终使解决方案逐步逼近问题的最优解。这便是贪心算法的大体思想,以每一小步为单位,做出符合当前条件的最有解。希望通过做出局部优化选择来达到全局优化选择。因此,贪心算法其实是局部最优而不是全局最优,不一定总产生优化解,产生的解是否是最优解需要通过严格的证明。
贪心算法的思想,很大程度上降低了问题的优化策略的思考难度,但这也导致了贪心算法有了一些限制:
- 不能保证求得的最后解为最优解。
- 不能用来求最大值或最小值问题。
- 只能求满足某些约束条件的可行解的范围。
贪心算法的基本步骤
贪心算法产生优化解具有两个条件:贪心选择性和优化子结构。
贪心选择性是指若一个优化问题的全局优化解可以通过局部优化选择得到,则该问题称为具有贪心选择性。具体表现为:
- 用同一原则将原问题变为一个相似但规模更小的子问题,而后每一步选择都是该子问题的最佳选择。
- 此选择仅依赖于已做出的选择,而与未做出的选择无关。
- 算法进行的过程中没有回溯。
而优化子结构意味着若一个优化问题的优化解包含它的子问题的优化解,则称其具有优化子结构。举例来说明优化子结构。若原问题S = {a1,a2,...,an},通过贪心选择一个最优解{ai}后,问题转化为求解子问题S - {ai},如果原问题最优解包含子问题的最优解,说明该问题满足优化子结构。
值得注意的是,这两个条件都需要用数学定理严格证明,还需要证明算法确实按照贪心选择性进行局部优化选择,之后才能使用贪心算法。通常采用归纳法和交换论证法来证明贪心算法的正确性。
由上述条件可得到贪心算法的一般步骤:
- 建立数学模型描述问题,并确定问题的优化子结构。
- 将最优化问题转化为:对其做出一个选择后,只剩下一个子问题需要求解。
- 证明做出贪心选择后,原问题总是存在最优解。
- 证明做出贪心选择后,剩余子问题满足:其最优解与贪心选择组合即可得到原问题的最优解。
接下来从具体的问题来分析贪心算法 。
贪心算法经典问题
任务安排问题
这是一个非常经典的问题。有n个需要在同一天使用同一个教室的活动a1,a2,…,an,教室同一时刻只能由一个活动使用。每个活动ai都有一个开始时间si和结束时间fi 。一旦被选择后,活动ai就占据半开时间区间[si,fi)。如果[si,fi]和[sj,fj]互不重叠,ai和aj两个活动就可以被安排在这一天。该问题就是要安排这些活动使得尽量多的活动能不冲突的举行。例如下图所示的活动集合S,其中各项活动按照结束时间单调递增排序。
根据题意可得到以下三种选取方案:
- 选择具有最早开始时间且与已安排活动相容的活动,以增加时间资源利用率
- 选择持续时间最短且与已安排活动相容的活动,以安排更多的活动
- 选择具有最早结束时间与已安排活动相容的活动,以尽快安排下一会议
容易考虑到,选择具有最早开始时间的活动,万一该活动持续时间过长(从早八持续到晚八);选择持续时间短的活动,万一该活动开始时间较晚(晚上七点到晚上八点)。所以最好的选择应是最早开始时间 + 最短持续时间 = 最晚开始时间。
由该题衍生出来的贪心思想是:为了选择最多相容活动,每次选fi最小的活动,使我们能够选更多的活动。
贪心算法的伪代码
(设f1<f2<...<fn)
贪心-Activity-Selector(S, F)
n<-length(S);
A<-{1}
j<-1
For i<-2 To n Do
If si > fj
Then A<-AU{i}; j<-i;
Return A
算法复杂性分析
- 如果结束时间已排序 T(n) = O(n)
- 如果结束时间未排序T(n) = O(n) + O(n*logn) = O(n*logn)
C语言实现结果
#include <stdio.h>
#include <stdlib.h>
// 任务安排问题
typedef struct _task{
int id;
int start;
int finish;
}task;
int cmpFinish(task t1, task t2){
return t1.finish < t2.finish;
}
void sort(task T[], int n){
int i,j;
task key;
for (i=1;i<n;i++){
key = T[i];
j=i-1;
while((j>=0) && (T[j].finish>key.finish)) {
T[j+1] = T[j];
j--;
}
T[j+1] = key;
}
}
int main(){
int n;
//count用来记录相容的任务个数
int count = 1;
printf("请输入待安排的任务总数(小于20个):");
scanf("%d", &n);
getchar();
task Task[20];
task vTask[20];
printf("请输入%d个任务的id,start,finish:\n",n);
for (int i = 0; i < n; i++){
scanf("%d %d %d", &Task[i].id, &Task[i].start, &Task[i].finish);
}
// 首先将任务按照结束时间非递减进行排序
sort(Task, n);
// 因为第一个任务一定在最优解里面(这个可以反证法证明出来的),所以首先将第一个任务加入
vTask[0] = Task[0];
// 然后看一下后面的任务,其start开始时间,有没有与前一个任务的结束时间fi重合
int j = 0; // 用来记录最新加入的任务,以便确定需要比较的finish时间。
for (int i = j + 1; i < n; i++)
{
if (Task[i].start >= Task[j].finish) // 需要注意等于也可以!
{
vTask[count] = Task[i];
j = i;
count ++;
}
}
for (int i = 0; i < count; i++)
{
printf("%d;", vTask[i].id);
}
putchar('\n');
return 0;
}
测试用例及实现结果
请输入待安排的任务总数(小于20个):6
请输入6个任务的id,start,finish:
1 1 3
2 2 4
3 5 8
4 7 12
5 9 13
6 13 14
1;3;5;6;