【数据结构与算法】贪心算法

引入:漫画:什么是“贪心算法”?如何求解“部分背包问题”?

贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

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

贪心算法的基本要素:

1.贪心选择性质。所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。

2.当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。

该算法存在问题:

  • 不能保证求得的最后解是最佳的;
  • 不能用来求最大或最小解问题;
  • 只能求满足某些约束条件的可行解的范围。

思路:从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到算法中的某一步不能再继续前进时,算法停止。

实现框架:

从问题的某一初始解出发;
while(能朝给定总目标前进一步 ){
   求出可行解的一个解元素;
}  
由所有解元素组合成问题的一个可行解;

活动选择

设有N个活动时间集合,每个活动都要使用同一个资源,比如说会议场,而且同一时间内只能有一个活动使用,每个活动都有一个使用活动的开始si和结束时间fi,即他的使用区间为(si,fi),现在要求你分配活动占用时间表,即哪些活动占用该会议室,哪些不占用,使得他们不冲突,要求是尽可能多的使参加的活动最大化,即所占时间区间最大化!

活动选择

输入:

11
1 4
3 5
0 6
5 7
3 8
5 9
6 10
8 11
8 12
2 13
12 14

如果我们每次都选择开始时间最早的活动,不能得到最优解

如果我们每次都选择持续时间最短的活动,不能得到最优解

可以用数学归纳法证明,我们的贪心策略应该是每次选取结束时间最早的活动。直观上也很好理解,按这种方法选择相容活动为未安排活动留下尽可能多的时间。这也是把各项活动按照结束时间单调递增排序的原因。

应该选择1 4 8 11

代码示例:

#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)  
        {  
            cout<<j<<" ";
            i=j;  
            num++;  
        }  
    }  
    return num;
}
 
int main()  
{  
		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);
	 	cout<<"排序后的活动顺序如下"<<endl;
		cout<<"id  start  end"<<endl;
		for(int i=1;i<=N;i++){
			cout<<i<<"    "<<act[i].start<<"    "<<act[i].end<<endl;
		} 
		cout<<"选择的活动为 1 "; 
    	int res=greedy_activity_selector();
		cout<<"活动数量:"<<res<<endl;  
}  

线段覆盖

在一维空间中告诉你N条线段的起始坐标与终止坐标,要求求出这些线段一共覆盖了多大的长度。

线段覆盖

输入:

10
2 3
3 5
4 7
5 6
6 9
7 8
8 12
9 10
10 13
11 15

这道题类似,但最优解就不一样了,是什么呢?动动你的小脑瓜。

代码示例:

#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.start<b.start;  
} 
 
int greedy_activity_selector()  
{  
	int totalL=(3-2),i=1;   
    for(int j=2;j<=N;j++)  
    {  
        if(act[j].start>=act[i].end)  
        {  
            totalL+=(act[j].end-act[j].start);
            i=j;    
        }  
        else{
        	if(act[i].end>=act[j].end) continue;
        	else{
        		totalL+=(act[j].end-act[i].end);
        		i=j;
			} 
		}
    }  
    return totalL;
}
 
int main()  
{  
		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);
	 	cout<<"排序后的线段顺序如下"<<endl;
		cout<<"id  start  end"<<endl;
		for(int i=1;i<=N;i++){
			cout<<i<<"    "<<act[i].start<<"    "<<act[i].end<<endl;
		} 
		cout<<"线段覆盖总长度:"<<greedy_activity_selector()<<endl;  
}  

钱币找零

假设1元、2元、5元、10元、20元、50元、100元的纸币分别有c0, c1, c2, c3, c4, c5, c6张。现在要用这些钱来支付K元,至少要用多少张纸币?如果不能恰好支付,输出"NO"。用贪心算法的思想,很显然,每一步尽可能用面值大的纸币即可。(在程序中已经事先将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;
}

多机调度

n个作业组成的作业集,可由m台相同机器加工处理。要求给出一种作业调度方案,使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。作业不能拆分成更小的子作业;每个作业均可在任何一台机器上加工处理。这个问题是NP完全问题,还没有有效的解法(求最优解),但是可以用贪心选择策略设计出较好的近似算法(求次优解)。当n<=m时,只要将作业时间区间分配给作业即可;当n>m时,首先将n个作业从大到小排序,然后依此顺序将作业分配给空闲的处理机。也就是说从剩下的作业中,选择需要处理时间最长的,然后依次选择处理时间次长的,直到所有的作业全部处理完毕,或者机器不能再处理其他作业为止。如果我们每次是将需要处理时间最短的作业分配给空闲的机器,那么可能就会出现其它所有作业都处理完了只剩所需时间最长的作业在处理的情况,这样势必效率较低。在下面的代码中没有讨论n和m的大小关系,把这两种情况合二为一了。

输入:n个作业 m台机器;然后输入每个作业所需要处理的时间

输出:处理完所有作业所需要的最短时间

代码示例:

#include<iostream>  
#include<algorithm>   
#include<cstring> 
using namespace std;  
int speed[10010];  
int mintime[110];  
 
bool cmp( const int &x,const int &y)  
{  
    return x>y;  
}  
 
int main()  
{  
	int n,m;         
	memset(speed,0,sizeof(speed));  
 	memset(mintime,0,sizeof(mintime));  
  	cin>>n>>m;  
   	for(int i=0;i<n;++i) cin>>speed[i];  
    sort(speed,speed+n,cmp);  
    for(int i=0;i<n;++i)   
    { 
    	*min_element(mintime,mintime+m)+=speed[i];   
   	}   
    cout<<*max_element(mintime,mintime+m)<<endl; 
}

小船过河

POJ1700是一道经典的贪心算法例题。题目大意是只有一艘船,能乘2人,船的运行速度为2人中较慢一人的速度,过去后还需一个人把船划回来,问把n个人运到对岸,最少需要多久。先将所有人过河所需的时间按照升序排序,我们考虑把单独过河所需要时间最多的两个旅行者送到对岸去,有两种方式:

1.最快的和次快的过河,然后最快的将船划回来;次慢的和最慢的过河,然后次快的将船划回来,所需时间为:t[0]+2t[1]+t[n-1];
2.最快的和最慢的过河,然后最快的将船划回来,最快的和次慢的过河,然后最快的将船划回来,所需时间为:2
t[0]+t[n-2]+t[n-1]。

算一下就知道,除此之外的其它情况用的时间一定更多。每次都运送耗时最长的两人而不影响其它人,问题具有贪心子结构的性质。

代码示例:

#include<iostream>
using namespace std;
int main()
{
	int a[1000],n,sum;
		scanf("%d",&n);
		sum=0;
        for(int i=0;i<n;i++) scanf("%d",&a[i]);
        while(n>3)
		{
			sum=min(sum+a[1]+a[0]+a[n-1]+a[1],sum+a[n-1]+a[0]+a[n-2]+a[0]);
            n-=2;
        }
        if(n==3) sum+=a[0]+a[1]+a[2];
        else if(n==2) sum+=a[1];
        //else sum+=a[0];
        printf("%d\n",sum);
}

注:该代码不符合提交要求,如要提交需满足输入T个测试样例,加上while(T–)即可

区间覆盖

POJ1328是一道经典的贪心算法例题。题目大意是假设海岸线是一条无限延伸的直线。陆地在海岸线的一侧,而海洋在另一侧。每一个小的岛屿是海洋上的一个点。雷达坐落于海岸线上,只能覆盖d距离,所以如果小岛能够被覆盖到的话,它们之间的距离最多为d。题目要求计算出能够覆盖给出的所有岛屿的最少雷达数目。(有图片更好理解,请点击链接查看完整题目)

对于每个小岛,我们可以计算出一个雷达所在位置的区间。问题转化为如何用尽可能少的点覆盖这些区间。先将所有区间按照左端点大小排序,初始时需要一个点。如果两个区间相交而不重合,我们什么都不需要做;如果一个区间完全包含于另外一个区间,我们需要更新区间的右端点;如果两个区间不相交,我们需要增加点并更新右端点。

代码示例:

#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
struct Point
{
	double x;
	double y;
}point[1000];
 
int cmp(const void *a, const void *b)
{
    return (*(Point *)a).x>(*(Point *)b).x?1:-1;
}
 
int main()
{
	int n,d;
	int num=1;
	while(cin>>n>>d)
	{
		int counting=1;
		if(n==0&&d==0) break;
		for(int i=0;i<n;i++)
		{
			int x,y;
			cin>>x>>y;
			if(y>d)
			{
				counting=-1;
			}
			double t=sqrt(d*d-y*y);
			//转化为最少区间的问题 
			point[i].x=x-t;
			//区间左端点 
			point[i].y=x+t;
			//区间右端点 
		}
		if(counting!=-1)
		{
			qsort(point,n,sizeof(point[0]),cmp);
			//按区间左端点排序 
			double s=point[0].y;
			//区间右端点 
			for(int i=1;i<n;i++)
			{
				if(point[i].x>s)
				//如果两个区间没有重合,增加雷达数目并更新右端点 
				{
					counting++;
					s=point[i].y; 
				}
				else if(point[i].y<s)
				//如果第二个区间被完全包含于第一个区间,更新右端点 
				{
					s=point[i].y;
				}
			}
		}
		cout<<"Case "<<num<<':'<<' '<<counting<<endl;
		num++; 
	}
}	

销售策略

假设有偶数天,要求每天必须买一件物品或者卖一件物品,只能选择一种操作并且不能不选,开始手上没有这种物品。现在给你每天的物品价格表,要求计算最大收益。

首先要明白,第一天必须买,最后一天必须卖,并且最后手上没有物品。那么除了第一天和最后一天之外我们每次取两天,小的买大的卖,并且把卖的价格放进一个最小堆。如果买的价格比堆顶还大,就交换。这样我们保证了卖的价格总是大于买的价格,一定能取得最大收益。

输入:测试样例个数t,对于每个测试样例输入n天(偶数天),然后输入每天的物品价格

输出:最大收益

代码示例:

#include<queue>
#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
long long int price[100010],t,n,res;
       
int main()
{
    ios::sync_with_stdio(false);
    cin>>t;
    while(t--)
    {
        cin>>n;
        priority_queue<long long int, vector<long long int>, greater<long long int> > q;
        res=0;
        for(int i=1;i<=n;i++)
        {
            cin>>price[i];
        }
        res-=price[1];
        res+=price[n];
        for(int i=2;i<=n-1;i=i+2)
        {
            long long int buy=min(price[i],price[i+1]);
            long long int sell=max(price[i],price[i+1]);
            if(!q.empty())
            {
                if(buy>q.top())
                {
                    res=res-2*q.top()+buy+sell;
                    q.pop();
                    q.push(buy);
                    q.push(sell);
                }
                else
                {
                    res=res-buy+sell;
                    q.push(sell);
                }
            }
            else
            {
                res=res-buy+sell;
                q.push(sell);
            }
        }     
        cout<<res<<endl;
    }
}

参考博文:

https://blog.csdn.net/qq_32400847/article/details/51336300

  • 25
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值