枚举就是机遇逐个尝试答案的一种求解策略
例如:求小于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;
}
这里用到了二进制数进行枚举和位运算的巧用。
整理改写自郭炜老师的《程序设计与算法(二)》