一、基本概念
所谓贪心算法是指,在对问题求解时,总是 做出在当前来看是最好的选择。也就是说,不从整体最优上加以考虑,通过贪心算法做出来的往往是在把 原问题拆分成几个小问题,分别求 每个小问题的最优解,再把这些“局部最优解”叠起来,就作为整个问题 当前 的最优解。
贪心算法无固定的算法框架,算法设计的关键是贪心策略的选择,必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择贪心策略必须具备 无后效性(即某个状态以后的过程不会影响以前的状态,只与当前状态有关)。
比如,求 最小生成树的Prim算法 和 Kruskal算法 都是漂亮的贪心算法。
二、基本思路
建立 数学模型 来描述问题;
把求解的问题分成 若干个子问题;
对每个子问题求解,得到 子问题的局部最优解;
把子问题的局部最优解 合成 原来问题的一个解。
三、存在的问题
不能保证求得的最后解是最佳的;
不能用来 求最值 的问题;
只能求 满足某些约束条件 的可行解的范围。
四、例题分析
设有M台完全相同的机器运行N个独立的任务,运行任务i所需要的时间为ti,要求确定一个调度方案,使得完成所有任务所需要的时间最短。假设任务已经按照其运行的时间从大到小来排序,算法基于最长运行时间作业优先的策略:按顺序先把每个任务分配到一台机器上,然后将剩余的任务一次放入最先空闲的机器。
这里要求定义的变量如下,所有数组的下标皆从0开始:
设,m是机器数,n是任务数,t[]的长度为n,其中每个元素表示任务的运行时间。s[][]长度为mn,下标从0开始,其中s[i][j]表示机器i运行的任务j的编号。d[]长度为m,其中匀速d[i]表示机器i运行的时间,count[]长度为m,其中元素count[i]表示机器i运行的任务数。max为完成所有任务的时间。min,i,j,k为临时定义变量,无意义。
考虑实例m=3(编号0-2),n=7(编号0-6),各任务的运行时间为{16,14,6,5,4,3,2}(这里其实就是指t[i]定义为{16,14,6,5,4,3,2},题目没有说)则求各个机器上的运行任务,从任务开始运行到完成所需要的时间。
这道题目,非常长,看起来非常让人头晕,实际上,根本一点都不难。如果你还学过《操作系统》的话,就更简单的。
意思就是将{16,14,6,5,4,3,2}这些数字扔到3个数组里面,使最终,这3个数组里面的数据之和的最大的一个,最小。你也可以想像倒水到3个杯子,要求你每次倒的水的多少只能从{16,14,6,5,4,3,2}这7个数字选一个,必须倒水倒7次,也就是这7个数字选完,最终尽可能让任一一个杯子都不溢出。
你当然是平均化倒水啊,每次倒水的时候,看哪个杯子水量最小倒哪个啊!
这时,就运用到所谓的“贪心算法”了。
整个过程如下图所示:
最后求出来的结果是17。顺理成章就得到如下的代码,每一次选择都会有一个求最小值的过程:
#include<stdio.h>
void schedule(int m,int n,int *t){
//初始化
int i,j,k,max=0;
int d[100],s[100][100],count[100];
for(i=0;i<m;i++){
d[i]=0;
for(j=0;j<n;j++){
s[i][j]=-1;//-1代表不执行任何任务,不与第0号任务混淆
}
}
//分配前m个任务
//必然是每个机器先分别接受1个任务
for(i=0;i<m;i++){
s[i][0]=i;
d[i]=d[i]+t[i];
count[i]=1;
}
//之后判断哪个机器任务耗时最少,让其接受任务
//尽可能地并行,平均分配任务
for(i=m;i<n;i++){
int min=d[0];
k=0;
for(j=1;j<m;j++){//确定空闲机器,实质是在求当期任务总时间最少的机器
if(min>d[j]){
min=d[j];
k=j;//机器k空闲
}
}
s[k][count[k]]=i;//在机器k的执行队列添加第i号任务
count[k]=count[k]+1;//机器k的任务数+1
d[k]=d[k]+t[i];//机器k的任务执行时间+t[i],也就是+第i号任务的耗时
}
for(i=0;i<m;i++){//确定完成所有任务需要的时间,实质是求分配完所有任务之后,耗时最多的机器
if(max<d[i]){
max=d[i];
}
}
printf("完成所有任务需要的时间:%d\n",max);
printf("各个机器执行的耗时一览:\n");
for(i=0;i<m;i++){
printf("%d:",i);
for(j=0;j<n;j++){
if(s[i][j]==-1){
break;
}
printf("%d\t",t[s[i][j]]);
}
printf("\n");
}
}
void main(){//测试用例
int time[7]={16,14,6,5,4,3,2};
schedule(3,7,time);
}
五、算法的使用前提
原问题复杂度过高;
求全局最优解的数学模型难以建立;
求全局最优解的计算量过大;
没有太大必要一定要求出全局最优解,“比较优”就可以。
六、分解问题方式
按串行任务分
时间串行的任务,按子任务来分解,即每一步都是在前一步的基础上再选择当前的最优解。
按规模递减分
规模较大的复杂问题,可以借助递归思想,分解成一个规模小一点点的问题,循环解决,当最后一步的求解完成后就得到了所谓的“全局最优解”。
按并行任务分
这种问题的任务不分先后,可能是并行的,可以分别求解后,再按一定的规则(比如某种配比公式)将其组合后得到最终解。