程序设计与算法---枚举

枚举就是机遇逐个尝试答案的一种求解策略

例如:求小于N的最大素数

           我们找不到一个数学公式,使得只要给出N就可以计算出这个数,但我们可以一个一个来列举出来,如我们逐个尝试N - 1,N - 2.........

这样可以非常直观简洁的实现最直白的解题思路,我们在具体解题时,不管题目的难度如何,能用枚举解决的题目还是占大多数的(虽然会超时)。

例题一:完美立方

 题目很清晰,我们最先想到的是a,b,c,d分别从0列举到N,可是这样子是很不好的,枚举虽然不要动脑筋,但也不能太过直接。我们还可以对程序进行进一步的优化和减枝。

首先a是不用想的,他的范围为[2,N],b的范围紧随其后为[2,a - 1],再根据题意b <= c <= d,可以依此得到c的范围为[b,a - 1],d的范围为[c,a - 1]。这样就完成了程序的具体思路,代码依照思路佷简单,如下:

#include<stdio.h>
int main()
{
	int N;
	scanf("%d",&N);
	for(int a = 2;a < N; a++)
	for(int b = 2;b < a; b++)
	for(int c = b;c < a; c++)
	for(int d = c;d < a; d++)
	if(a*a*a == b*b*b + c*c*c + d*d*d)
	printf("Cube=%d,Triple=(%d,%d,%d)\n",a,b,c,d);
	return 0;
}

 根据这道题我们可以晓得,枚举虽然简单,但同时也需要考虑一下优化,以此来进一步提高代码的质量。

 例题二:生理周期

 

该题目可以明显看出是枚举,即从给出的日期d + 1天枚举到21252,但当实际操作时就会发现这是个惊人的计算量,三个for循环,每个从d + 1枚举到21252,这是很不好的。所以,我们采取优化和减枝。

从题目中可以看出,在体力高峰出现后的23内,是不可能出现幸运日的,同样,在智力和情商高峰出现后的一段时间内是不用考虑的,所以可以直接跳过。

具体代码如下:

#include<iostream>
#include<cstdio>
using namespace std;
#define N 21252
int main()
{
	int p,e,i,d,caseNo = 0;
	while(cin>>p>>e>>i>>d && p != -1)
	{
		++caseNo;
		int k;
		for(k = d + 1;(k - p)%23; ++k);
		for(;(k - e)%28;k += 23);
		for(;(k - i)%33;k += 23*28);
		cout<<"Case"<<caseNo<<": the next triple peak occurs in "<<k-d<<"days."<<endl;
	}
	return 0;
}

 例题三:假币问题

 

这道题描述的是,已知有且只有一枚假币,但不知道这枚假币是轻的还是重的,现给出三组已经测量好的数据,要求根据数据,得到那一枚是假币,而且知道这枚假币是轻的还是重的。

对于这道题,可以先假设每一枚硬币是轻的,然后代入,看结果是否正确,如果不符合,则假设它是重的,再代入,遍历所有硬币,一定能找出最后的特殊硬币。

由于硬币的表示是A-L,即数组的左边或者右边最长为6个字符,所以用一个字符型二维数组[3][7]存左右硬币,遍历A-L硬币,分别先假设其为轻,然后用两个指针指向左右硬币,再根据给出的结果,判定结果,如果不成立,再反过来,左指针指向右天平,右指针指向左天平,以此遍历下去。

代码如下:

#include<iostream>
#include<cstring>
using namespace std;
char Left[3][7];
char Right[3][7];
char result[3][7];
	bool IsFake(char c,bool light){
	for(int i = 0;i < 3;i++){
		char * pLeft,*pRight;
		if(light){
			pLeft = Left[i];
			pRight = Right[i];
		}else{         //假设其为重时,直接将左右指针调换,省去重写一遍的步骤
			pLeft = Right[i];
			pRight = Left[i];
		}
		switch(result[i][0]){//取给定结果的前一个字母
			case 'u':
				if(strchr(pRight,c) == NULL)//匹配该硬币是否在右边
					return false;
					break;
		    case 'e':
		    	if(strchr(pLeft,c) || strchr(pRight,c))//该硬币应该两边都没有
		    	return false;
		    	break;
		    case 'd':
		    	if(strchr(pLeft,c) == NULL)//匹配该硬币是否在左边
		    	return false;
		    	break;
		}
	}
	return true;
}
int main()
{
	int t;
	cin >> t;
	while(t--){
		for(int i = 0;i < 3;++i)cin >> Left[i] >> Right[i] >> result[i];//输入数据
		for(char c = 'A';c <= 'L';c++){//遍历每一枚硬币
			if(IsFake(c,true)){//假设假硬币为轻
				cout << c << "is the counterfeit coin and it is light.\n";
				break;
			}
			else if(IsFake(c,false)){//假设假硬币为重
				cout << c << "is the counterfeit coin and it is heavy.\n";
				break;
			}
		}
	}
	return 0;
}

例题四:熄灯问题

 

  该题目相较于前面几题有点难度,因为我们虽然是枚举,但无从下手,不知道从那个方面解题。首先,我们要想明白这件事:灯的开关按两下的结果等于没按。所以我们只需要考虑每盏灯按一次或者不按的情况。但有一个很麻烦的事情就是一个开关影响的是3-5盏灯,我们需要考虑边界情况,还需要考虑上一次按下的按钮对这次的影响。

考虑到这,如果再有一些算法基础的话,就可以想到第一行的开关结果可以直接决定最后的结果。这是因为第一行按钮按下后,按第二行的按钮的目的只能是解决第一行现在还亮着的灯,以此类推,我们可以得到这道题的最后解法,同样,我们如果选择枚举列的话,会减少一部分计算,即枚举2的5次方。

这里我们采用行运算,此时依然是很大的一个数,但鉴于所取的情况只有0或者1,所以我们可以用64表示第一行每个按钮的状况,具体来说就是0-1,表示第一个按钮的状态,2-3表示第二个按钮的状态,以此类推,对于每个数的取值,我们可以通过位运算来得到。

#include<memory>
#include<string>
#include<cstring>
#include<iostream>
using namespace std;
int GetBit(char c,int i){
	return (c >> i)&1; //取字符c的第i个比特
} 
void SetBit(char &c,int i,int v){ //设置字符c的第i位为v
	if(v)
	c |= (1 << i);//c的第i位赋值为1
	else
	c &= ~(1 << i); //c的第i位赋值为0
}
void Flip(char &c,int i){//将c的第i位反转
	c ^= (1<<i);
}
void OutputResult(int t,char result[])//输出结果
{
	cout<<"PUZZIE #"<<t<<endl;
	for(int i = 0;i < 5; ++i){
		for(int j = 0;j < 6; ++j){
		cout<<GetBit(result[i],j);//取出result[i]的第j个比特
		if(j < 5)
		cout<<" ";
	}
	cout<<endl;
    }
}
int main(){
	char oriLights[5];//储存原始灯矩阵,每个元素有八个比特
	char lights[5];//储存变换灯矩阵
	char result[5];//储存结果矩阵
	char switchs;//储存开关
	int T;
	cin>>T;
	for(int t = 1;t <= T; ++t){
		memset(oriLights,0,sizeof(oriLights));
		for(int i =0;i < 5; i++){
			for(int j = 0;j < 6; j++){
				int s;
				cin>>s;//将数据读入到s中
				SetBit(oriLights[i],j,s);//将oriLight的第j个比特设为s
			}
		}
		for(int n = 0;n < 64; ++n){
			memcpy(lights,oriLights,sizeof(oriLights));//复制oriLight到lights
			switchs = n;
			for(int i = 0;i < 5; ++i){
				result[i] = switchs;//储存开关状态
				for(int j = 0;j < 6; ++j){//枚举开关状态
					if(GetBit(switchs,j)){//影响灯状态
						if(j>0)
						Flip(lights[i],j-1);
						Flip(lights[i],j);
						if(j <5 )//未到边界
						Flip(lights[i],j+1);
					}
				}
				if(i < 4)
				lights[i+1] ^= switchs;
				switchs = lights[i];
			}
				if(lights[4] == 0){ //判断假设是否成功
					OutputResult(t,result);
					break;
				}
			}
		}
		return 0;
}

这里用到了二进制数进行枚举和位运算的巧用。

整理改写自郭炜老师的《程序设计与算法(二)》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值