NOIP实用算法 4.贪心方法

4.贪心方法
a.工程计划模型
b.部分背包与每步最优
c.构造贪心算法

a.工程计划模型

我们常常碰到这样的问题:完成一个工程需要若干个步骤,每个步骤都有若干种方法,图示——

步骤a  步骤b  步骤c  ... 步骤n
       方法b1 方法c1
方法a1 方法b2 方法c2     方法n1
方法a2 方法b3 方法c3
              方法c4

每个方法有一个权值(如效率、质量),其大小往往和其他步骤中选取的方法有关。有些时候权值无意义,表示方法不可选择。要求给出一个方法组合,是权值和最大。
在这里,暂且把它称作“工程计划”。很多实际问题都可以归纳为这个模型。
对于不同形式的工程计划,我们有不同的解法。
若权值与整个过程或前后步骤的方法选择都有关,我们使用搜索算法——时间复杂度高得吓人。
若每个权值只与上(或下)一步或少数几步的方法选择都有关,我们使用动态规划——有比较高的效率,在下一章会讲到。
若每个权值与其他步骤的方法选择都没有关系,我们使用贪心方法。


b.部分背包与每步最优

强调:每个权值与其他步骤的方法选择都没有关系。这样每步最优就可以得到全局最优——每一步都取最大的权值就可以了。
换而言之,贪心算法要求,局部的贪心选择,可以组成全局的最优解。
在实际问题中,这是需要证明的。如果这个无法证明,贪心算法所得的解不是最优解,一般只是较优解(较优解可为搜索剪枝提供方便)。

下面是贪心算法最经典的例子:部分背包问题。(下一章会讲到另外两种背包问题。)
问题:有N件物品和一个最大载重为M的背包,每件物品都有相应的重量和价值。现要求给出一个存放方案,使背包中物品总价值最大。部分背包要求,每件物品都可只装入它的一部分(部分重量有成比例的部分价值)。所涉及到的数字均为整数。
(注:有时该问题表述为体积形式,即背包体积有限,每件物品有体积和价值。在本系列我选择表述为重量形式。)
思路:背包中物品总价值最高,即单位重量物品价值最高。显然,应该多装单位重量价值高的物品。这样,我们先装入单位重量价值最高的物品,再装入第二高的……直到重量达到M(有必要时最后一件物品只装一部分),已达到物品总价值最高。
这个证明应该很严谨吧~
该算法时间复杂度O(n),效率很高;而且实现很容易。这些是贪心法最大的特点。

很多竞赛题看似可以用贪心法,其实贪心法得不到最优解,原因是每一步的选择对其他步骤有影响。
数字三角形问题:有一个数字三角形(如下图)。现有一只蚂蚁从顶层开始向下走,每走下一级时,可向左下方向或右下方向走。求走到底层后它所经过的数的最大值。

    1
   6 3
  8 2 6
 2 1 6 5
3 2 4 7 6

如果用贪心法,每次向最大的方向走,得到结果为1+6+8+2+3=20。可是明明还有另一条路,1+3+6+6+7=23。
问题出在哪?每次的选择对后面的步骤会有影响!第三级选了8,就选不到第四、五级较大的数了。
这个问题正确的解法会在下一章介绍。
有一个很实用的小技巧:竞赛题会给出数据规模。通过数据规模,我们可以大致判断该用何种算法。贪心算法可承受的数据规模很大,一般都会上万。如果给出的数据规模是100或1000,优先考虑动态规划吧。

c.构造贪心算法

构造与证明是贪心算法的难点,常常要求我们要有敏锐的观察力、多角度思考的变通能力、丰富的数学知识和推理能力。
下面举几个贪心算法的例子,供大家揣摩、掌握规律。

删数问题:给出一个N位的十进制高精度数,要求从中删掉S个数字(其余数字相对位置不得改变),使剩余数字组成的数最小。
算法构造:
1.每次找出最靠前的这样的一对数字——两个数字紧邻,且前面的数字大于后面的。删除这对数字中靠前的一个。
2.重复步骤1,直至删去了S个数字或找不到这样的一对数。
3.若还未删够S个数字,则舍弃末尾的部分数字,取前N-S个。
证明思路:显然,在只删一个数字时,唯有步骤1的方法能使数变小;可推理得出,删多个数字时,所有最优的方法都可看做是对步骤1的重复。也就是说,以上方法是最优策略之一。
在文末的附件中给出了这个算法的源代码。

工序问题:n件物品,每件需依次在A、B机床上加工。已知第i件在A、B所需加工时间分别为A[i]、B[i],设计一加工顺序,使所需加工总时间最短。
算法构造:
1.设置集合F、M、S:先加工F中的,再加工M中的,最后加工S中的。
2.对第i件,若A[i]>B[i],则归入S;若A[i]=B[i],则归入M。
3.对F中的元素按A[i]升序排列,S中的按B[i]降序排列。
证明思路:
1.F中的能“拉开”A、B加工同一件工件的结束时刻,为后面的工件加工“拉开时间差”,利于节省总时间。S中的刚好相反。因而,F中元素放在最前一定是最优策略之一。
2.F中A[i]小的前置,可以缩短开始时B的空闲时间,但会使F所有工件“拉开的时间差”缩短。不过可以证明,后者带来的损失不大于前者获得的优势。对称地,对S也一样。因而步骤3是可行的。

种树问题:一条街道分为n个区域(按1-n编号),每个都可种一棵树。有m户居民,每户会要求在区域i-j区间内种至少一棵树。现求一个能满足所有要求且种树最少的方案。
算法构造:
1.对于要求,以区间右端(升序)为首要关键字,左端(升序)为次要关键字排序。
2.按排好的序依次考察这些要求,若未满足,则在其最右端的区域种树,这时可能会满足多个要求。
证明思路:解法并不唯一,关键是证明没有比该解法更好的解法。按步骤1排序之后,会发现对于每个要求,在最右边的区域内种树所得的结果总不会差于在其他区域种树。至于为什么这样排序,留给你——读者们思考吧。
在文末的附件中给出了这个算法的源代码。


//删数问题 
//This C source file for program "Delete Number" is written by LastLeaf
//blog.programet.cn
//Edit 3

#include "stdio.h"

#define N_MAX 100
#define S_MAX 90
#define L_MAX (N_MAX-S_MAX)

int In[N_MAX+1],Out[N_MAX];

void cal(int *stIn,int lenIn,int *stOut,int lenOut)
{
	int iIn=0,iOut=0,dc;
	
	dc=lenIn-lenOut;
	
	for(;iIn<lenIn;)
		if(stIn[iIn]>stIn[iIn+1] && dc)
		{
			if(iOut>0)stIn[iIn]=stOut[--iOut];
			else iIn++;
			dc--;
		}
		else
			stOut[iOut++]=stIn[iIn++];
	
	return;
}

int main()
{
	int i,N,S;
	char c;
	FILE *fp;

	fp=fopen("DeleteNumber.in","r");
	fscanf(fp,"%d %d\n",&N,&S);
	for(i=0;i<N;i++)
	{
		fscanf(fp,"%c",&c);
		In[i]=c-'0';
	}
	In[i]=10;
	fclose(fp);
	
	cal(In,N,Out,N-S);

	fp=fopen("DeleteNumber.out","w");
	for(i=0;i<N-S;i++)
	{
		fprintf(fp,"%d",Out[i]);
	}
	fprintf(fp,"\n");
	fclose(fp);

    return 0;
}

//种树问题
//一条街道分为n个区域(按1-n编号),每个都可种一棵树。有m户居民,每户会要求在区域i-j区间内种至少一棵树。现求一个能满足所有要求且种树最少的方案。
//written by LastLeaf
//blog.programet.cn

#include <stdio.h> 
int main()
{
	FILE *fp;
	int N,H,B[5001],E[5001],T[5001],i,ii,m,n,P[30001],total=0,right,down;
	
	fp=fopen("trees.in","r");
	fscanf(fp,"%d\n%d\n",&N,&H);
	for(i=0;i<H;i++)
	{
		fscanf(fp,"%d %d %d",&B[i+1],&E[i+1],&T[i+1]);
		for(ii=0;ii<i;ii++)if(B[i+1]<=B[ii])break;
		for(;B[i+1]==B[ii];ii++)if(E[i+1]<E[ii])break;
		if(ii!=i)
			for(m=i;m>ii;m--)
			{
				B[m]=B[m-1];
				E[m]=E[m-1];
				T[m]=T[m-1];
			}
		B[ii]=B[i+1];
		E[ii]=E[i+1];
		T[ii]=T[i+1];
	}
	close(fp);
	
	for(i=0;i<=N;i++)P[i]=0;//初始化 
	for(i=0;i<H;i++)
	{
		while(T[i]>0)
		{
			right=E[i];
			//选择位置
			for(down=i+1;B[down]<=right && down<H;down++)
				if(E[down]<right)right=E[down];
			for(;P[right]==1;right--);
			//种树
			total++;
			P[right]=1;
			for(ii=i;ii<down;ii++)
			{
				if(right>=B[ii])
				{
					T[ii]--;
					if(T[ii]==0)
					{
						B[ii]=-1;
						E[ii]=N+1;
					}
				}
			} 
		}
	}
	
	fp=fopen("trees.out","w");
	fprintf(fp,"%d\n",total);
	for(i=1;i<=N;i++)
		if(P[i]==1)fprintf(fp,"%d ",i);
	fprintf(fp,"\n");
	fclose(fp);
	return 0;
}




http://blog.programet.org/2009/07/noip%E5%AE%9E%E7%94%A8%E7%AE%97%E6%B3%95-4-%E8%B4%AA%E5%BF%83%E6%96%B9%E6%B3%95.html#comment-11144

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值