程序设计课 期末考试复习 专题一:贪心

前言:期末来临,为了复习这门课,我会按照学长给的考试范围复习下整个学期的内容,每个专题我会根据学长的教学资料和查找网上资料来写一篇总结和自己的理解,并将专题中留下来的题目再做一遍(有精力的话就从网上找一些相关类型的题做做)。

第一个专题是贪心。

我对贪心的理解:在求解一个问题时,把这个问题分成一步步来解决,在每一步的实现时,仅仅依据当前已有的信息来做出选择,不去考虑这步决策对未来对整体有什么影响。即,每一步做到局部最优,不考虑整体最优。贪心算法对于大部分的优化问题都能产生最优解,但不能总获得总体最优解,通常能够获得近似最优解。

贪心定义:贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
解题的一般步骤是:
1.建立数学模型来描述问题;
2.把求解的问题分成若干个子问题;
3.对每一子问题求解,得到子问题的局部最优解;(选取最优指标)
4.把子问题的局部最优解合成原来问题的一个解。
(摘自:https://www.cnblogs.com/hust-chen/p/8646009.html 在这篇文章中还有很多经典问题)

通过问题理解:找零钱。
假如你去小卖铺买东西,买了少于100块的东西,但是给了店长100块,你希望店长找给你纸币数最少(纸币面额有1元、5元、10元、20元、50元、100元。并且找的钱数也是整元)。
这个问题利用贪心思想来解决的话,就是要利用这些面额的纸币来组成应找的钱数。那么我就可以从最大面额的纸币来选取,当最大面额超过剩余的钱数,就从比它小一点的面额中选,直到达到钱数。
虽然这个问题用贪心能够解决,但是这是依靠我们纸币面额来决定的。如果我仅有1元、5元、11元的面额纸币,而我需要找15元,这样,按照贪心思想来,我要找:11、1、1、1、1 总共五张纸币才行,而如果我选用:5、5、5 三张就可以凑齐。这就说明贪心思想在解决问题时有它的局限性。

经典例题:


1.区间调度(from课件)
题意:给定一个长度固定的区间 [s,t] ,给定n个活动,然后给出这n个活动的开始时间和结束时间,要求在区间[s,t]内不能重叠的完成最多的活动数。图如:

区间[0,11],活动abcdefgh,有自己的开始时间和结束时间。要求完成活动个数最多。
理解:如同题意,无特别理解。
题解:我们有了一个区间,要求完成活动数目最多,利用贪心思想解决这个问题,我们可以从最左端点的区间来开始每一步选择一个最优活动,然后我们就要定一个选择最优活动的标准,我们可以选择最早开始作为标准,也可以选择最早结束作为标准,也可以选择活动长度作为标准。但是正确的标准只有一个:最早结束。我们可以这样直观上理解:选择最早结束的活动,为接下来的活动预留更多的时间。
OK,那么上图的顺序就是:bcaedfgh 。然后要求时间不能重叠,那么我们下一个活动的开始时间就要大于等于上一个活动结束时间,再以这个为附加标准来选择活动。这样我们就选出了:b、e、h三个活动。可以看出三个活动是最多的个数了。

程序代码(DEV环境)(本代码仅根据题意理解写出,在写具体题目时,可能会有不合适,但思想正确,可以根据本题代码改写成正确格式):

#include<iostream>
#include<string>
#include<algorithm>
using namespace std;

struct act
{
	int s,t;
};
act a[1000];

bool com(act a,act b)
{
	return a.t<b.t;
}

int main()
{
	int n;  //n表示从0~n为整个活动区间 
	cin>>n;
	int num;
	cin>>num;
	for(int i=0;i<num;i++)
	    cin>>a[i].s>>a[i].t;
	sort(a,a+num,com);
	int ans=1;
	int end=a[0].t;
	for(int i=1;i<num;i++)
	{
		if(a[i].s>=end)
		{
			ans++;
			end=a[i].t;
		}
	}
	cout<<ans<<endl;
	return 0;
}

扩充:sort函数的使用方法。(详细用法:https://www.cnblogs.com/yfz1552800131/p/5373064.html)

       sort是排序函数,头文件#include<algorithm>

扫描二维码关注公众号,回复: 10665434 查看本文章

一般使用方法:sort(a,a+n)  :意为排序a数组,排序a[0]~a[n-1]。

自己定义:sort(a,a+n,com)   :com是自己定义的bool类型的函数,用作排序时参考的比较函数。

2.最小延迟调度。 (from课件)  

     题意:与区间调度不同,仍然是单个资源,给每个活动的属性:持续时间t、截止时间(ddl),每个活动区间的开始时间任意。要求合理安排活动(整体的资源开始时间默认为0,也可以自己设置),让活动的最大延迟最小(延迟:安排的活动结束时间超过其ddl之后,延迟为超过的时长,如果不超过,那么延迟为0)。

    例如:整体开始时间:0。活动序号:1、2、3、4、5、6;对应的活动的持续时间:3、2、1、4、3、2;对应活动ddl:6、8、9、9、14、15。 安排这些活动的开始时间,是的在整体区间内不重叠,而且最大延迟最小。

经观察最佳安排结果为:1、2、3、4、5、6;

题解:最小延迟调度,要求延迟最小,我们需要选择指标。经思考指标为:按照ddl为指标。理解如同上一个问题:为了先完成截止时间最早的,不让延迟过度。

这样我们就可以进行代码编写了。

程序代码(DEV环境)(本代码仅根据题意理解写出,在写具体题目时,可能会有不合适,但思想正确,可以根据本题代码改写成正确格式):

#include<iostream>
#include<algorithm> 
using namespace std;

struct ACT
{
	int t;
	int ddl;
};
ACT a[1005];

bool com(ACT a,ACT b)
{
	return a.ddl<b.ddl;
}

int main()
{
	int num;
	cin>>num;
	for(int i=0;i<num;i++)
	{
		cin>>a[i].t>>a[i].ddl;
	}
	sort(a,a+num,com);
	int maxlate=0;
	int end=0;
	for(int i=0;i<num;i++)
	{
		if(end+a[i].t-a[i].ddl>maxlate)
		    maxlate=end+a[i].t-a[i].ddl;
		end=end+a[i].t;
	}
	cout<<maxlate<<endl;
	return 0;
}

3.区间选点(from考试复习资料)

     题意:数轴上有n个闭区间[ai,bi]。尽量选取少的点,使得每个区间内都至少有一个点(不同区间可以包含的是同一个点)。

     题解:选取最优标准:n个区间的右端点bi。把区间按照b从小到大排序(b相同时,a从大到小排序),选取有重叠区间部分的第一个区间的最后一个点(右端点)。

     部分代码(与上面两个题解相似,不再赘述代码,仅给出sort用到的排序函数)

bool com(act x,act y)
{
	if(x.b==y.b)
	    return x.a>y.b;
	else return x.b<y.b;
}

4.区间覆盖(from考试复习资料)

题意:数轴上有n个闭区间[ai,bi],选择尽量少的区间来覆盖一条指定的线段[s,t]。

具体题目:poj2376 :cleaning shifts。(https://vjudge.net/problem/POJ-2376)

题目意思:有T的长度,从1开始直到T要求被用最少个数的cow来覆盖,给定n为cow的总共个数,给定n个区间,每个表示从s可以干活到t(注意:假如给的区间是[3,5]意味着它可以干 从“2区间”末尾干到“5区间”末尾,干了三个区间),若不能覆盖则输出-1,若能覆盖则输出最少的cow的个数。

本题的贪心标准是:每个区间左端点;

求解思路:首先按照贪心标准,排好序后,设定一个整体时间的左端点s,贪心的选择在左端点小于s的并且右端点最远的区间。然后将左端点修改成右端点区间最远的右端点。一直到左端点改成比T还要大的数据停止。这种达成某一条件之前要一直进行下去的结构最好选用while结构,不过要注意好左端点要改变或者进行判断时的边界情况。

(while内的思想参考:https://blog.csdn.net/v5zsq/article/details/46828461)

代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;

struct cowWorkTime
{
	int s;
	int t;
};
cowWorkTime ct[25005];

bool com(cowWorkTime a,cowWorkTime b)
{
	return a.s<b.s;
}

int main()
{
	int n,t;
	cin>>n>>t;
	for(int i=0;i<n;i++)
	{
		scanf("%d%d",&ct[i].s,&ct[i].t);
	}
	sort(ct,ct+n,com);
	int sta=0;
	int ans=0;
	int cur=0;
	while(sta<t&&cur<n)
	{
		ans++;
		int maxt=sta;
		if(ct[cur].s>sta+1)
			break;
		while(ct[cur].s<=sta+1&&cur<n)
		{
			if(ct[cur].t>maxt)
			    maxt=ct[cur].t;
			cur++;
		}
		sta=maxt;
	}
	if(sta<t)
	    cout<<-1<<endl;
	else cout<<ans<<endl;
	return 0;
}

以上四个题是讲课的课件和复习资料内的题目。均与区间有关,所以想要贪心的进行选择时所采取的排序标准是与区间有关的,例如:区间左端点、区间右端点、区间长度等等。当在做这类题目是,如果没有思路,可以从这几个标准里选一个直接进行试水,毕竟有时候也是会过几个测试数据,这样还可以捞一点分数。

接下来的几个题,是我从网上资料找的题,也是属于贪心思想范畴。

5.钱币找零问题

假设1元、2元、5元、10元、20元、50元、100元的纸币分别有c0, c1, c2, c3, c4, c5, c6张。现在要用这些钱来支付K元,至少要用多少张纸币?

题解:运用贪心思想,每一步先选择最大面额的纸币,要么选没所拥有的张数,要么选至多能选的张数。直到选出K元来。

代码:

#include<iostream>  
#include<algorithm>  
using namespace std;  
const int N=7;   
int Count[N];
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;
	for(int i=0;i<N;i++)
	{
		cin>>Count[i];
	}  
    int res=solve(money);  
    if(res!=-1) cout<<res<<endl;  
    else cout<<"NO"<<endl;  
    return 0;
}

6.背包问题(其中内的物品可以取一部分)

这个问题就贪心的取单位价值最大的物品即可,即用物品的价值除去物品所占的空间,取最大单位价值的物品填满背包即可。

不赘述代码。

7.小船过河(poj1700)

题意:一群人过河,但是小船只能载两个人,并且在载两个人的时候前进速度是慢的那个人的速度,在两个人过河之后,还需要一个人将船再从河对面开回来。给出人的个数,给出每个人开船过河需要的时间,求所有人都过河后用的总共时间最少(多组数据)

贪心算法:首先我们每一步的过程要把最慢和次慢的人运过去,利用 最快 或者 最快和次快 的人开船两种方式。那么所耗费的时间分别是:

(1)先是最快的人和次快的人过河,最快的人开回船来,然后最慢和次慢的人过河,让河对岸次快的人把船开回来,所耗费时间是:time[0]+2*time[1]+time[n-1];

(2)让最快和最慢的人过河,再让最快的人把船开回来,再让最快和次慢的人过河,再让最快的人开回来,所耗费的时间是:2*time[0]+time[n-2]+time[n-1];

仅有这两种方式会让最慢和次慢的人过河最快。所以本题解题过程就是,将所有过河的人的时间按照从小到大排序,按照每步送两个最慢的人过去,直到送到少于四人之后,求出这个过程中所用最少的时间。

代码:

#include<iostream>
#include<algorithm>
using namespace std;
int time[1005];

int min(int a,int b)
{
	return a<b?a:b;
}

int main()
{
	int T;
	cin>>T;
	int n;
	while(T--)
	{
		int ans=0;
		cin>>n;
		for(int i=0;i<n;i++)
		    cin>>time[i];
		sort(time,time+n);
		while(n>3)
		{
			ans+=min(time[0]+2*time[1]+time[n-1],2*time[0]+time[n-2]+time[n-1]);
			n-=2;
		}
		if(n==3) ans+=time[0]+time[1]+time[2];
		if(n==2) ans+=time[1];
		if(n==1) ans+=time[0];
		cout<<ans<<endl;		
	}
	return 0;
}

8.区间覆盖问题(poj1328:https://vjudge.net/problem/POJ-1328

题意:有一个地平线,在地平线上可以安置雷达,在地平线的另一侧是海洋,海洋里面有岛屿。已知雷达课探测的范围是以直径为d的圆,给定岛屿的位置坐标,要求能够覆盖所有这些岛屿的雷达的最少的个数。

理解:可以按照岛屿为圆心,d为半径做一个圆,看它与x轴(即地平线)所交的区间,每个岛屿对应一个区间,然后就可以演变成了区间选点问题,要求所有区间都有一个点,但是点的个数最少。与问题3就想通了。

然后我就从网上搜了搜题解,发现几乎都是用区间的左端点来做排序标准,从小到大。然后在计算雷达个数时,从头开始找,如果超过当前规定的右端点,那么雷达数加一,如果查看到的区间被包含在当前的区间内,那么需要更新右端点为更小的那个。

我的做法 代码:

#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;

struct QJ
{
	double x,y;
}qj[1005];

bool cmp(QJ a,QJ b)
{
	if(a.y==b.y) return a.x>b.x;
	else return a.y<b.y;
}

int main()
{
	int n;
	double d;
	int num=1;

	while(cin>>n>>d)
	{
		if(n==0&&d==0) break;
		int ans=-1;
		bool p=1;
		for(int i=0;i<n;i++)  
        {  
            double x,y;  
            cin>>x>>y;  
            if(y>d)  
            {  
                p=0;  
            }  
            double t=sqrt(d*d-y*y);  
            //转化为最少区间的问题   
            qj[i].x=x-t;  
            //区间左端点   
            qj[i].y=x+t;  
            //区间右端点   
        }
        if(p)
        {
        	sort(qj,qj+n,cmp);
        	ans=1;
        	double t=qj[0].y;
        	for(int i=0;i<n;i++)
        	{
        		if(qj[i].x>t)
        		{
        			ans++;
        			t=qj[i].y;
				}
			}
		}
		cout<<"Case "<<num++<<": "<<ans<<endl;
	}
	return 0;
}

以上就是我对“贪心”这一课题的总结了。

其实贪心算法还有很多方面的使用,比如求最短路径的dijkstra算法,求最小生成树的kruskal算法。这两个我会在我之后的课题“图论”和“树形结构”中进行总结。

发布了6 篇原创文章 · 获赞 0 · 访问量 184

猜你喜欢

转载自blog.csdn.net/morning_zs202/article/details/91978992