一、简介
在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。
从贪心算法的定义可以看出,贪心算法不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解,而由问题自身的特性决定了该题运用贪心算法可以得到最优解。
如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。
二、贪心算法的理论基础
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,希望得到结果是最好或最优的算法。
贪心算法是一种能够得到某种度量意义下的最优解的分级处理方法,通过一系列的选择得到一个问题的解,而它所做的每一次选择都是当前状态下某种意义的最好选择。即希望通过问题的局部最优解求出整个问题的最优解。
这种策略是一种很简洁的方法,对许多问题它能产生整体最优解,但不能保证总是有效,因为它不是对所有问题都能得到整体最优解。
利用贪心策略解题,需要解决两个问题:
- 该题是否适合于用贪心策略求解;
- 如何选择贪心标准,以得到问题的最优/较优解。
一、贪心选择性质
贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
- 在动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题后,才能做出选择。
- 在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择,然后再去解出这个选择后产生的相应的子问题。
二、最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
运用贪心策略在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。
贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。
贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。
动态规划主要运用于二维或三维问题,而贪心一般是一维问题。
三、贪心算法的求解过程
使用贪心算法求解问题应该考虑如下几个方面:
- 候选集合A:为了构造问题的解决方案,有一个候选集合A作为问题的可能解,即问题的最终解均取自于候选集合A。
- 解集合S:随着贪心选择的进行,解集合S不断扩展,直到构成满足问题的完整解。
- 解决函数solution:检查解集合S是否构成问题的完整解。
- 选择函数select:即贪心策略,这是贪心法的关键,它指出哪个候选对象最有希望构成问题的解,选择函数通常和目标函数有关。
- 可行函数feasible:检查解集合中加入一个候选对象是否可行,即解集合扩展后是否满足约束条件。
三、例题
-
活动安排问题
活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合,是可以用贪心算法有效求解的很好例子。
该问题要求高效地安排一系列争用某一公共资源的活动。
贪心算法提供了一个简单、漂亮的方法使得尽可能多的活动能兼容地使用公共资源。
设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。
每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si<fi。如果选择了活动i,则它在半开时间区间 [ si ,fi )内占用资源。若区间 [ si ,fi ) 与区间 [ sj,fj )不相交,则称活动i与活动j是相容的。当 si ≥ fj 或 sj ≥ fi 时,活动i与活动j相容。
活动安排问题就是在所给的活动集合中选出最大的相容活动子集合。
void GreedySelector(int n, action a[], bool b[])
{
b[1] = true; //第1个活动是必选的
//记录最近一次加入到集合b中的活动
int preEnd = 1;
for(int i=2; i<=n; i++)
if (a[i].s>=a[preEnd].f)
{
b[i] = true;
preEnd = i;
}
}
-
背包问题
给定一个载重量为 M 的背包,考虑 n 个物品,其中第 i 个物品的重量 ,价值 wi (1 ≤ i ≤ n),要求把物品装满背包,且使背包内的物品价值最大。
有两类背包问题(根据物品是否可以分割),如果物品不可以分割,称为0—1背包问题(动态规划);如果物品可以分割,则称为背包问题(贪心算法)。
假设 xi 是物品 i 装入背包的部分 ( 0≤ xi ≤ 1 ) ,当 xi = 0 时表示物品 i 没有被装入背包;当 xi = 1 时表示物品 i 被全部装入背包。
根据问题的要求,该问题可形式化描述为:
∑ i = 1 n w i x i = M s . t . m a x ∑ i = 1 n p i x i \displaystyle \sum^n_{i=1}w_ix_i=M\quad s.t.max\displaystyle \sum^n_{i=1}p_ix_i i=1∑nwixi=Ms.t.maxi=1∑npixi
其中,0 ≤ xi ≤ 1。有3种方法来选取物品:
- 当作0—1背包问题,用动态规划算法,获得最优值220;
- 当作0—1背包问题,用贪心算法,按性价比从高到底顺序选取物品,获得最优值160。由于物品不可分割,剩下的空间白白浪费。
- 当作背包问题,用贪心算法,按性价比从高到底的顺序选取物品,获得最优值240。由于物品可以分割,剩下的空间装入物品3的一部分,而获得了更好的性能。
struct bag{
int w; //物品的重量
int v; //物品的价值
//double x; 装入背包的量,0≤x≤1
//int index; 物品编号
double c; //性价比
}a[1001]; //存放物品的数组
bool cmp(bag a, bag b){ //排序因子(按性价比降序)
return a.c >= b.c;
}
sort(a, a+n, cmp); //使用标准模板库函数排序(最好使用stable_sort()函数,在性价比相同时保持输入的顺序):
//形参n是物品的数量,c是背包的容量M,数组a是按物品的性价比降序排序
double knapsack(int n, bag a[], double c){
double cleft = c; //背包的剩余容量
int i = 0;
double b = 0; //获得的价值
//当背包还能完全装入物品i
while(i<n && a[i].w<cleft){
cleft -= a[i].w;
b += a[i].v;
//a[a[i].index].x = 1.0;
//物品原先的序号是a[i].index,全部装入背包
i++;
}
//装满背包的剩余空间
if (i<n){
//a[a[i].index].x = 1.0*cleft/a[i].w;
b += 1.0*a[i].v*cleft/a[i].w;
}
return b;
}
-
最优装载问题
有一批集装箱要装上一艘载重量为 c 的轮船,其中集装箱i的重量为 wi 。最优装载问题要求确定在装载体积不受限制的情况下,将尽可能多的集装箱装上轮船。
该问题的形式化描述为:
m a x ∑ i = 1 n x i , s . t . ∑ i = 1 n w i x i ≤ c max\displaystyle \sum^n_{i=1}x_i,\qquad s.t.\quad \displaystyle \sum^n_{i=1}w_ix_i\leq c maxi=1∑nxi,s.t.i=1∑nwixi≤c
其中xi∈{0,1},1≤i≤n。最优装载问题可用贪心算法求解。采用重量最轻者先装的贪心选择策略,可得到装载问题的最优解。表示集装箱的数据结构如下:
struct load { int index; //集装箱编号 int w; //集装箱重量 }box[1001];
排序因子(按集装箱的重量升序):
bool cmp (load a, load b) { if (a.w<b.w) return true; else return false; }
使用标准模板库函数排序(box[0]未使用):
stable_sort(box, box+n+1, cmp); //这是稳定排序函数,当重量相同时,保持输入数据原来的顺序。
while (scanf("%d%d", &c, &n)!=EOF) { memset(box, 0, sizeof(box)); memset(x, 0, sizeof(x)); for (int i=1; i<=n; i++) { scanf("%d", &box[i].w); box[i].index = i; } //按集装箱的重量升序排序 stable_sort(box, box+n+1, cmp); if (box[1].w>c) { printf("No answer!\n"); continue; } //贪心算法的实现,重量最轻者先装载 int i; for (i=1; i<=n && box[i].w<=c; i++) { x[box[i].index] = 1; c -= box[i].w; } //输出装载的集装箱数量 printf("%d\n", i-1); //输出装载的集装箱编号 for (i=1; i<=n; i++) if (x[i]) printf("%d ", i); printf("\n"); }
-
删数问题
给定n位正整数a,去掉其中任意k≤n个数字后,剩下的数字按原次序排列组成一个新的正整数。对于给定的n位正整数a和正整数k,设计一个算法找出剩下数字组成的新数最小的删数方案。
输入
第1行是1个正整数a,第2行是正整数k。
输出
对于给定的正整数a,编程计算删去k个数字后得到的最小数。n n n位数 a a a可表示为 x 1 x 2 … x i x j x k … x n x_1x_2\ldots x_ix_jx_k\ldots x_n x1x2…xixjxk…xn ,要删去 k k k位数,使得剩下的数字组成的整数最小。
将该问题记为 T T T,最优解 A = x i 1 x i 2 … x i m ( i 1 < i 2 < … < i m , m = n − k ) A=x_{i1}x_{i2}\ldots x_{im}\ (i1<i2<\ldots<im,\ m=n-k) A=xi1xi2…xim (i1<i2<…<im, m=n−k),在删去 k k k个数后剩下的数字按原次序拍成的新数,其最优值记为 N N N。
本问题采用贪心算法求解,采用最近下降点优先的贪心策略:即 x 1 < x 2 < … < x i − 1 < x i x_1<x_2<\ldots<x_{i-1}<x_i x1<x2<…<xi−1<xi,如果 x i + 1 < x i x_{i+1}<x_i xi+1<xi(下降点),则删去 x i x_i xi,即得到一个新的数且这个数为 n − 1 n-1 n−1位中最小的数 N 1 N_1 N1,可表示为 x 1 x 2 … x i − 1 x i + 1 … x n x_1x_2\ldots x_{i-1}x_{i+1}\ldots x_n x1x2…xi−1xi+1…xn。
显然删去 1 1 1位数后,原问题 T T T变成了对 n − 1 n-1 n−1位数删去 k − 1 k-1 k−1个数的新问题 T ′ T' T′。
新问题和原问题性质相同,只是问题规模由 n n n减小为 n − 1 n-1 n−1,删去的新数字个数由 k k k减少为 k − 1 k-1 k−1。
基于此种删除策略,对新问题 T ′ T' T′,选择最近下降点的数继续进行删除,直至删去 k k k个数为止。
string a; //n位数a int k; cin>>a>>k; //如果k≥n,数字被删完了 If (k >= a.size()) a.erase(); else while(k > 0) { //寻找最近下降点 int i; for (i=0; (i<a.size()-1) && (a[i] <= a[i+1]); ++i); a.erase(i, 1); //删除xi k- -; } //删除前导数字0 while(a.size() > 1 && a[0] == '0') a.erase(0, 1); cout<<a<<endl;
-
多处最优服务次序问题
设有 n n n个顾客同时等待一项服务,顾客i需要的服务时间为 t i t_i ti, 1 ≤ i ≤ n 1\leq i\leq n 1≤i≤n,共有 s s s处可以提供此项服务。应如何安排 n n n个顾客的服务次序才能使平均等待时间达到最小?平均等待时间是 n n n个顾客等待服务时间的总和除以 n n n。
给定的 n n n个顾客需要的服务时间和 s s s的值,编程计算最优服务次序。
输入
第一行有 2 2 2个正整数 n n n和 s s s,表示有 n n n个顾客且有 s s s处可以提供顾客需要的服务。接下来的 1 1 1行中,有 n n n个正整数,表示 n n n个顾客需要的服务时间。
输出
最小平均等待时间,输出保留 3 3 3位小数。假设原问题为 T T T,并已经知道某个最优服务序列,即最优解为 A = { t 1 , t 2 , … , t n } A=\{t_1,t_2,\ldots,t_n\} A={t1,t2,…,tn},其中 t i t_i ti为第 i i i个用户需要的服务时间,则每个用户等待时间 T i T_i Ti为:
T 1 = t 1 ; T_1=t_1; T1=t1;
T 2 = t 1 + t 2 ; T_2=t_1+t_2; T2=t1+t2;
… \ldots …
T n = t 1 + t 2 + … + t n T_n=t_1+t_2+\ldots +t_n Tn=t1+t2+…+tn
那么总的等待时间,即最优值 N N N为:
N = n t 1 + ( n − 1 ) t 2 + … + 2 t n − 1 + t n N=nt_1+(n-1)t_2+\ldots+2t_{n-1}+t_n N=nt1+(n−1)t2+…+2tn−1+tn
由于平均等待时间是 n n n个顾客等待时间的总和除以 n n n,故本题实际上就是求使顾客等待时间的总和最小的服务次序。
设计贪心策略如下:
- 对服务时间最短的顾客先服务的贪心选择策略。
- 首先对需要服务时间最短的顾客进行服务,即做完第一次选择后,原问题 T T T变成了需对 n − 1 n-1 n−1个顾客服务的新问题 T ′ T' T′。
- 新问题和原问题相同,只是问题规模由 n n n减小为 n − 1 n-1 n−1。
- 基于此种选择策略,对新问题 T ′ T' T′,在 n − 1 n-1 n−1个顾客中选择服务时间最短的先进行服务,如此进行下去,直至所有服务都完成为止。
//顾客等待的队列为client,提供服务的窗口s个 double greedy(vector<int> client, int s) { //服务窗口的顾客等待时间 vector<int> service(s+1, 0); //服务窗口顾客等待时间的总和 vector<int> sum(s+1, 0); //顾客的数量 int n = client.size(); //按顾客的服务时间升序排序 sort(client.begin(), client.end()); //贪心算法的实现 int i=0; //顾客的指针 int j=0; //窗口的指针 while(i < n) { service[j] += client[i]; sum[j] += service[j]; ++i, ++j; if(j == s) j = 0; } //计算所有窗口服务时间的总和 double t=0; for(i=0; i<s; ++i) t += sum[i]; t /= n; return t; }