贪心算法

5,623 阅读11分钟

贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。

贪心算法的基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能获得局部最优解。每一步只考虑一个数据,他的选取应该满足局部优化的条件。若下一个数据和部分最优解连在一起不再是可行解时,就不把该数据添加到部分解中,直到把所有数据枚举完,或者不能再添加算法停。

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

一句话:不求最优,仅仅求可行解。

选择使用贪心算法

 我们能够依据贪心法的2个重要的性质去证明:贪心选择性质和最优子结构性质

1、贪心选择

  什么叫贪心选择?从字义上就是贪心也就是目光短线。贪图眼前利益。在算法中就是仅仅依据当前已有的信息就做出选择,并且以后都不会改变这次选择。(这是和动态规划法的主要差别)  

  所以对于一个详细问题。要确定它是否具有贪心选择性质,必须证明每做一步贪心选择是否终于导致问题的总体最优解。

2、最优子结构

  当一个问题的最优解包括其子问题的最优解时,称此问题具有最优子结构性质。

       运用贪心策略在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。动态规划主要运用于二维或三维问题,而贪心一般是一维问题

基本思路

贪心算法的基本思路是从问题的某一个初始解触发一步一步地进行,根据抹个优化测度,每一步都要确保能获得局部最优解,每一步值考虑一个数据,他的选取应该满足局部优化的条件。若下一个数据和部分最优解连载一起不再是可行解时,就不把改数据添加到部分解中,知道把所有数据枚举玩,或者不能在添加算法停止。

过程

  1. 建立数学模型来描述问题;
  2. 把求解的问题分成若干个子问题;
  3. 对每一子问题求解,得到子问题的局部最优解;
  4. 把子问题的解局部最优解合成原来解问题的一个解。

算法特性

  1. 随着算法的进行,将积累起其它两个集合:一个包含已经被考虑过并被选出的候选对象,另一个包含已经被考虑过但被丢弃的候选对象。
  2. 有一个函数来检查一个候选对象的集合是否提供了问题的解答。该函数不考虑此时的解决方法是否最优。
  3. 还有一个函数检查是否一个候选对象的集合是可行的,也即是否可能往该集合上添加更多的候选对象以获得一个解。和上一个函数一样,此时不考虑解决方法的最优性。
  4. 选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解。
  5. 最后,目标函数给出解的值。
  6. 为了解决问题,需要寻找一个构成解的候选对象集合,它可以优化目标函数,贪婪算法一步一步的进行。起初,算法选出的候选对象的集合为空。接下来的每一步中,根据选择函数,算法从剩余候选对象中选出最有希望构成解的对象。如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;否则就加到集合里。每一次都扩充集合,并检查该集合是否构成解。如果贪婪算法正确工作,那么找到的第一个解通常是最优的。

一般求解过程

  使用贪心法求解能够依据下面几个方面进行(终于也相应着每步代码的实现),以找零钱为例:

  1、候选集合(C)

    通过一个候选集合C作为问题的可能解。(终于解均取自于候选集合C)

    比如。在找零钱问题中,各种面值的货币构成候选集合。

  2、解集合(S)

    每完毕一次贪心选择,将一个解放入S。终于获得一个完整解S

  3、解决函数(solution)

    检查解集合S是否构成问题的完整解。

    比如,在找零钱问题中。解决函数是已付出的货币金额恰好等于应付款。

  4、选择函数(select)

    即贪心策略。这是贪心法的关键,选择出最有希望构成问题的解的对象。

(这个选择函数通常和目标函数有关)

   比如,在找零钱问题中,贪心策略就是在候选集合中选择面值最大的货币。

  5、可行函数(feasible)

    检查解集合中增加一个候选对象是否可行。(增加下一个对象后是不是满足约束条件)

    比如。在找零钱问题中,可行函数是每一步选择的货币和已付出的货币相加不超过应付款。

例子

1.活动选择问题

有n个需要在同一天使用同一个教室的活动a1,a2,…,an,教室同一时刻只能由一个活动使用。每个活动ai都有一个开始时间si和结束时间fi 。一旦被选择后,活动ai就占据半开时间区间[si,fi)。如果[si,fi]和[sj,fj]互不重叠,ai和aj两个活动就可以被安排在这一天。该问题就是要安排这些活动使得尽量多的活动能不冲突的举行。

问题分析:
活动安排问题要求安排一系列争用某一公共资源的活动。用贪心算法可提供一个简单、漂亮的方法,使尽可能多的活动能兼容的使用公共资源。设有n个活动的集合{0,1,2,…,n-1},其中每个活动都要求使用同一资源,如会场等,而在同一时间内只有一个活动能使用这一资源。每个活动i都有一个要求使用该资源的起始时间starti和一个结束时间endi,且starti<endi。如选择了活动i,则它在半开时间区间[starti,endi)内占用资源。若区间[starti,endi)与区间[startj,endj)不相交,称活动i与活动j是相容的。也就是说,当startj≥endi或starti≥endj时,活动i与活动j相容。活动安排问题就是在所给的活动集合中选出最多的不相容活动。
活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合,是可以用贪心算法有效求解的很好例子。该问题要求高效地安排一系列争用某一公共资源的活动。贪心算法提供了一个简单、漂亮的方法使得尽可能多的活动能兼容地使用公共资源。


算法设计:
若被检查的活动i的开始时间starti小于最近选择的活动j的结束时间endj,则不选择活动i,否则选择活动i加入集合中。运用该算法解决活动安排问题的效率极高。当输入的活动已按结束时间的非减序排列,算法只需O(n)的时间安排n个活动,使最多的活动能相容地使用公共资源。如果所给出的活动未按非减序排列,可以用O(nlogn)的时间重排。

#include<cstdio>  
#include<iostream>   
#include<algorithm>   
using namespace std;      
int N;  
struct Act  
{  
    int start;  
    int end;  
}act[100010];  
  
bool cmp(Act a,Act b)    
{    
    return a.end<b.end;    
}   
  
int greedy_activity_selector()    
{    
    int num=1,i=1;     
    for(int j=2;j<=N;j++)    
    {    
        if(act[j].start>=act[i].end)    
        {    
            i=j;    
            num++;    
        }    
    }    
    return num;  
}  
  
int main()    
{    
    int t;  
    scanf("%d",&t);  
    while(t--)  
    {  
        scanf("%d",&N);  
        for(int i=1;i<=N;i++)  
        {  
            scanf("%lld %lld",&act[i].start,&act[i].end);  
        }  
        act[0].start=-1;  
        act[0].end=-1;  
        sort(act+1,act+N+1,cmp);   
        int res=greedy_activity_selector();  
        cout<<res<<endl;    
    }  
}    

2.钱币找零问题

这个问题在我们的日常生活中就更加普遍了。假设1元、2元、5元、10元、20元、50元、100元的纸币分别有c0, c1, c2, c3, c4, c5, c6张。现在要用这些钱来支付K元,至少要用多少张纸币?用贪心算法的思想,很显然,每一步尽可能用面值大的纸币即可。在日常生活中我们自然而然也是这么做的。在程序中已经事先将Value按照从小到大的顺序排好。

#include<iostream>
#include<algorithm>
using namespace std;
const int N=7; 
int Count[N]={3,0,2,1,0,3,5};
int Value[N]={1,2,5,10,20,50,100};
  
int solve(int money) 
{
	int num=0;
	for(int i=N-1;i>=0;i--) 
	{
		int c=min(money/Value[i],Count[i]);
		money=money-c*Value[i];
		num+=c;
	}
	if(money>0) num=-1;
	return num;
}
 
int main() 
{
	int money;
	cin>>money;
	int res=solve(money);
	if(res!=-1) cout<<res<<endl;
	else cout<<"NO"<<endl;
}

3.背包问题

有一个背包,背包容量是M=150kg。有7个物品,物品不可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。

问题分析
目标函数: ∑pi最大,使得装入背包中的所有物品pi的价值加起来最大。

约束条件:装入的物品总重量不超过背包容量:∑wi<=M( M=150)

贪心策略:

⑴根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?

⑵每次挑选所占重量最小的物品装入是否能得到最优解?

⑶每次选取单位重量价值最大的物品,成为解本题的策略。
⑴贪心策略:选取价值最大者。
W=30
物品:A B C
重量:28 12 12
价值:30 20 20
根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。

⑵贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。

⑶贪心策略:选取单位重量价值最大的物品。

反例:
W=30
物品:A B C
重量:28 20 10
价值:28 20 10
根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。
【注意:如果物品可以分割为任意大小,那么策略3可得最优解】
对于选取单位重量价值最大的物品这个策略,可以再加一条优化的规则:对于单位重量价值一样的,则优先选择重量小的!这样,上面的反例就解决了。
但是,如果题目是如下所示,这个策略就也不行了。
W=40
物品:A B C
重量:25 20 15
价值:25 20 15
  • 算法设计:
  1. 计算出每个物品单位重量的价值
  2. 按单位价值从大到小将物品排序
  3. 根据背包当前所剩容量选取物品
  4. 如果背包的容量大于当前物品的重量,那么就将当前物品装进去。否则,那么就将当前物品舍去,然后跳出循环结束。

#include<iostream>
#include<algorithm>
using namespace std;
typedef struct{
    int w;
    int v;
    double avg;
}P;
bool cmp(P a,P b){
    return a.avg>b.avg;
}
int main(){
    P *p;
    int n,i,m;//n 物品个数 m背包容量
    while(cin>>n>>m){
        p=new P[n];
        for(i=0;i<n;i++){
            cin>>p[i].w>>p[i].v;
            p[i].avg=p[i].v/p[i].w*1.0;
        }
        sort(p,p+n,cmp);
        int maxvalue=0;
        for(i=0;i<n;i++){
            if(p[i].w<=m){
                m-=p[i].w;
                maxvalue+=p[i].v;
            }else{
                break;
            }
        }
        cout<<maxvalue<<endl;
    }
    return 0;
}