算法二:暴力枚举(优化版)(内含称硬币、种草、熄灯问题等典型例题)

例题一、完美立方

在这里插入图片描述
可以得出一些关键信息,a,b,c,d > 1, b <= c <= d, b < n,
c < n, d < n;
详细C代码如下:

#include<stdio.h>

int main()
{
	int a, b, c, d;
	int n;
	scanf("%d", &n);
	for(a = 2; a <= n; a++)
	{
		for(b = 2; b < n; b++)
		{
			for(c = b; c < n; c++)
			{
				for(d = c; d < n; d++)
				{
					if(a * a * a == b * b * b + c * c * c + d * d *d)
						printf("Cube = %3d, Triple = (%3d,%3d,%3d)\n", a, b, c, d);
				}
			}
		}
	}
	return 0;
} 

部分示例运行结果如下:
在这里插入图片描述

例题二、生理周期

在这里插入图片描述
在这里插入图片描述
虽说都是暴力枚举,但是其中还是有技巧的。首先由题意知要输出从给定的日子起,下一次三个高峰同一天的日子的天数,那就是要求下一次三高出现的日期k - d

这里可以直接暴力法
if((k - p) % 23 == 0 && (k - e)%28 == 0 && (k - i)%33 == 0)这种思路是没问题的,但不是最佳的暴力法

我们可以先循环找到某个体力高峰日,然后再用这个值去找情商高峰日,因为是建立在日子处于体力高峰,所以在寻找的过程中要进行k += 23,同时也要判断是否(k - e) % 28 == 0,如果找到了符合条件的,则进入下一个阶段的查找——智商高峰日,这里可以运用一个小技巧——最小公倍数。如果不记得最小公倍数的同学也不必着急,这里我举个例子,大家就会非常清楚这个概念了,同时我想引入最大公约数,一起记
在这里插入图片描述
也就是说进入最后一个阶段时,只要考虑(k - i) % 33是否等于零,以lcm(23, 28)来增加k的大小。这里就是核心思路。

详细C代码如下:

#include<stdio.h>

int gcd(int a, int b)
{
	if(a % b == 0)
		return a;
	else
		return gcd(b, a % b);
}

#define N 21252

int main()
{
	int p, e, i, d;
	int cnt = 0;
	scanf("%d %d %d %d", &p, &e, &i, &d); 
	while(p != -1 & e != -1 & i != -1)
	{
		int k = 0;
		int g = 0;
		for(k = d + 1; (k - p) % 23 != 0; k++);
		for(; (k - e) % 38 != 0; k += 23);
		g = 23 * 28 / gcd(23, 28);		//最小公倍数 
		cnt++;
		for(; (k - i) % 33 != 0; k += g);
		{
			if(k <= N)
			{
				printf("Case %d : The next triple peak occurs in %d days.\n", cnt, k - d);
			}
		}
		scanf("%d %d %d %d", &p, &e, &i, &d);
			
	}
	return 0;
}

部分示例结果如下:
在这里插入图片描述

例题三、称硬币

在这里插入图片描述
在这里插入图片描述
这个题可谓是暴力枚举题里的精品了,值得细品

首先我们要设三个二维数组,分别来存取每一次的测试数据以及测量结果。为何我设置列号为5而不是4呢,因为5是这个数组的下限,设为4的话会出现错误,具体为什么以后我会告诉你们。但这不是重点。然后我需要把这些数据存入这三个数组。接下来我需要用一个循环,在字符’A’~'K’之间枚举,找出假币,因为这里不仅仅是找假币,还要查出假币是重是轻。所以需要if(…) else…判断语句,分为两种考虑,重or轻,这里的核心就是设置一个函数,以便代码复用,要不然代码会很冗杂。

这里我先讲一下函数的最后面部分,这样你们就会懂为何我的代码

char *pLeft, *pRight;
		if(light)
		{
			pLeft = Left[i];
			pRight = Right[i];
		}
		else
		{
			pLeft = Right[i];
			pRight = Left[i];
		}

要这么写。

这个是个switch语句,是不是感觉之前那些代码没用到result数组,到这里就是它派上用场的时候了!
在这里插入图片描述
这里我画图解释一下
在这里插入图片描述
这里还有一块知识点值得注意:

strchr(str, c)函数

为了降低明白点我结合官网文件讲,C++官网链接各位兄弟可以收藏一下,以后方便查询,或者收藏我的文章hhhh

strchr()函数是在C语言<string.h>头文件或者C++语言cstring中
用法如下:

const char * strchr ( const char * str, int character );
      char * strchr (       char * str, int character );

在这里插入图片描述
他可以找到字符串str中第一个出现的字符character,注意str是指针,character是字符常量或变量,这可以查某个字符是否在str所指向的字符串中,如果在,返回的是指针,若不在,则返回NULL

这里有一个关于该函数的example,大家可以练一下手熟悉一下这个函数

#include <stdio.h>
#include <string.h>

int main ()
{
  char str[] = "This is a sample string";
  char * pch;
  printf ("Looking for the 's' character in \"%s\"...\n",str);
  pch=strchr(str,'s');
  while (pch!=NULL)
  {
    printf ("found at %d\n",pch-str+1);
    pch=strchr(pch+1,'s');
  }
  return 0;
}

运行结果如下:

Looking for the 's' character in "This is a sample string"...
found at 4
found at 7
found at 11
found at 18

好,这里函数我介绍完了

下面是这题的详细C代码 :

#include<stdio.h>
#include<string.h>

char Left[3][5];
char Right[3][5];
char result[3][5];

int IsFake(char c, int light)		//返回值为1表示假币轻,返回值为0表示假币重 
{
	int i;
	for(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 0;
				break;
			case 'e':
				if(strchr(pLeft, c) || strchr(pRight, c))
					return 0;
				break;
			case 'd':
				if(strchr(pLeft, c) == NULL)
					return 0;
				break;
		}
	}
	return 1;
}

int main()
{
	int t;
	scanf("%d", &t);
	getchar();
	while(t--)
	{
		int i;
		char n;
		for( i = 0; i < 3; ++i)
		{
			scanf("%s", Left[i]);
			scanf("%s", Right[i]);
			scanf("%s", result[i]);
		}
//		for(i = 0; i < 3; ++i)
//		{
//			printf("%s\n", Left[i]);
//		} 
//		for(i = 0; i < 3; ++i)
//		{
//			printf("%s\n", Right[i]);
//		}
//		for(i = 0; i < 3; ++i)
//		{
//			printf("%s\n", result[i]);
//		}
		char c;
		for(c = 'A'; c <= 'L'; c++)
		{
			if(IsFake(c, 1))
			{
				printf("%c is the counterfeit coin and it is light.\n", c);
				break;
			}
			else if(IsFake(c, 0))
			{
				printf("%c is the counterfeit coin and it is heavy.\n");
				break;
			}
		}
	}
	return 0;
}

运行结果如下:
在这里插入图片描述

例题四、种草问题

LiHua在自家门前方块的空地上中草,已知该空地为n x m的面积,空地上被平均分为了n x m个小块,其中.表示无草,g表示有草。已知每过一个月,长草的那一小块区域会往它的前后左右四个方向蔓延扩张一块区域,请问过了k个月之后,LiHua家前的空地上的具体情况如何?

输入格式:
第一行:输入n和m的值,n和m的值用空格隔开
第N(1 < N <= n)行:输入每行对应的情况,输入m个数据
最后一行输入k

样例1:
在这里插入图片描述
这个题还是蛮简单的,一看题目就可以出思路了。首先要一个二维数组a[n][m],然后给a数组个元素赋值。因为要看k个月之后的变化情况,所以需要一个循环,而改变数组的代码我选择定义一个函数来实现,这样可以使代码更加简洁,而且复用性强。这里我需要两个数组,一个用来记录初始的二维数组,另一个用来继承过了一个月改变后的二维数组b[n][m],相当于一个临时数组。然后再将b数组中的值在两层循环下传递给a数组。因为数组从某种意义上来说就是指针,所以调用函数可以进行值传递。

详细C代码如下:

#include<stdio.h>

int n, m;

void f(char a[][m], char b[][m])
{
	int i, j;
	for(i = 0; i < n; i++)
	{
		for(j = 0; j < m; j++)
		{
			if(a[i][j] == 'g')
			{
				if(j != 0)
					b[i][j - 1] = 'g';
				if(j != m - 1)
					b[i][j + 1] = 'g';
				if(i != 0)
					b[i - 1][j] = 'g';
				if(i != n - 1)
					b[i + 1][j] = 'g';
			}
		}
	}
	for(i = 0; i < n; i++)
	{
		for(j = 0; j < m; j++)
		{
			a[i][j] = b[i][j];
		}
	}
}

int main()
{
	scanf("%d %d", &n, &m);
	char a[n][m];
	char b[n][m];
	int i, j;
	getchar();
	for(i = 0; i < n; i++)
	{
		for(j = 0; j < m; j++)
		{
			scanf("%c",&a[i][j]);
			b[i][j] = a[i][j];
		}
		getchar();
	}
	int k;
	scanf("%d", &k);
	getchar();
	int t;
	for(t = 0; t < k; t++)
	{
		f(a,b);
	}
	for(i = 0; i < n; i++)
	{
		for(j = 0; j < m; j++)
			printf("%c", a[i][j]);
		printf("\n");
	}
	return 0;
	
	
}

部分示例运行结果如下:
在这里插入图片描述

变式4-1、熄灯问题★★★★★

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述直接思路:枚举所有可能的按钮(开关)状态,对每个状态计算一下最后灯的情况,看是否都熄灭。
一每个按钮有两种状态(按下或不按下)
一共有30个开关,那么状态数是230,太多,会超时

那么我们该如何减少枚举的状态数目呢?

基本思路:如果存在某个局部,一旦这个局部的状态被确定,那么剩个其他部分的状态只能是确定的一种,或者不多的n种,那么就只需枚举这个局部的状态即可。

经观察,第一行就是一个“局部”,当第一行各开关状态确定以后,则熄灭第一行某些亮着的灯,唯一办法就是按下第二行第i列的开关,而要熄灭第二行的灯就需要按下第三行对应的第i行的灯,依次类推,直到最后一行,如果到最后一行灯都是灭的,那么方案成功,即输出结果

既然我们要对第一行入手,那么第一行一共有26种按开关情况。一开始很多人都是想用6层循环,但这里不需要,根据题意,由于每一行是6个元素,那么我们可以直接用一维字符数组装这个5 x 6的数据集。这比用二维数组省事方便些,而且我们直接用一层循环就行,不需要用六层循环。这道题我是用位运算,但是对于位运算可能很多人不是很熟练,这里我会详细讲解!

大致思路如下图所示:
在这里插入图片描述
首先我们要按题目要求输入数据,那么就用两层循环进行输入。我定义一个char型的oriLights[5]数组用来存放每个案例,如何存入二进制数呢,我们可以定义一个
SetBit(char *c, int i, int v)函数,c是指向数组的指针,i表示第i位元素,v表示所需设置的数字,如果v = 0,就把第i位元素转化为0,若v = 1,则把第i位元素转化为0。

这里补充一下关于位运算的知识
1)value << shift
左移运算符<<
其中value表示要被操作的整数值,shift是要移动的位数
如果想用左移运算符修改原值,则可以使用<<=

在这里插入图片描述

2)value >> shift
右移运算符>>
其中value表示要被操作的整数值,shift是要移动的位数
如果想用右移运算符修改原值,则可以使用>>=

在这里插入图片描述

3)位非(求反)运算符(~)
~运算符将每一位转换成它的反面(1转换为0,0转换为1)
例如:3
00000011	——>		11111100
如果想用~运算符修改原值,则可以使用~=
4)或运算符OR(|)
opt1| opt2
如果两个被操作的值对应位至少有一个为1,则新值相应位为1,否则为0
如果想用|运算符修改原值,则可以使用|=
简单来记就是:
有1为1
全0为0

在这里插入图片描述

5)异或运算符XOR(^)
两个数的对应位数的值有一个为1,则新值为1,如果对应的位都为0或1,则新值中相应位为0
如果想用^运算符修改原值,则可以使用^=
b1 ^ b2的值
b10101
b20110
位新值0011

在这里插入图片描述

6)按位与运算符(&)
opt1 & opt2
规律:
全1为1
有0为0
如果想用&运算符修改原值,则可以使用&=

在这里插入图片描述
回到题目,当我们要存入数据时,采用位运算,用预先定义好的SetBit函数将第i行的第j位元素存入原始数据,如果要输入的是0,则字符c |= (1 << i);这里(1 << i)
是把1移到第i位,不能直接让c = (1 << i),要不然达不到改变c的第i位上数字的功能,这里必须用位运算中的|
如果第j位元素为0,则c &= ~(1 << i)

接下来输完数据就是进行枚举了,从0~63,memcpy()函数可以把字符数组内容拷贝给另一个字符数组,用lights数组来存每次枚修改的结果
在这里插入图片描述
然后在每一行枚举,把每次枚举的选项传给result[i]数组,然后再进行列循环,如果得到的第j个字符是1,再考虑j是否大于0或者小于5,首先它肯定要反转,因为要让它熄灯,如果大于0,那么它的前一个数要进行反转,小于5的话就让它后一个反转,等结束“列”循环,还要考虑i 是否小于4,因为如果小于4,那么llights[i + 1]也要反转,最后把得到的lights[i]传给switchs,以此往复。直到lights[4]== 0,那么说明枚举出来的方案是正确的

详细C代码如下:

#include<stdio.h>
#include<string.h>

char oriLights[5];
char lights[5];
char result[5];

int GetBit(char c, int i)		//得到c的第i位字符 
{
	return (c >> i) & 1;
}

void SetBit(char *c, int i, int v)		//把c的第i位设置成v(0或1) 
{
	if(v)
	{
		*c |= (1 << i);			//把c的第i位设置成1
	}
	else
	{
		*c &= ~(1 << i);		//把c的第i位设置成0
	}
}

void FlipBit(char *c, int i)		//把字符变量c的第i位进行反转 
{
	*c ^= (1 << i);
}

void OutputResult(int t, char result[])
{
	printf("PUZZLE #%d\n", t);
	int i;
	for(i = 0; i < 5; i++)
	{
		int j;
		for(j = 0; j < 6; j++)
		{
			printf("%d ", GetBit(result[i], j));
		}
		printf("\n");
	}
}

int main()
{
	int T;
	scanf("%d", &T);		//案例数 
	int t;
	for(t = 1; t <= T; t++)
	{
		int i, j;
		for(i = 0; i < 5; i++)
		{
			for(j = 0; j < 6; j++)
			{
				int s;
				scanf("%d", &s);
				SetBit(&oriLights[i], j, s);
			}
		}
		int n;
		for(n = 0; n < 64; n++)		//枚举第一行所有可能开关状态 
		{
			int switchs = n;
			memcpy(lights, oriLights, sizeof(oriLights));	 
			int i;		//拷贝一份oriLights数组数据给lights数组,保证原始数据不受影响 
			for(i = 0; i < 5; i++)
			{
				result[i] = switchs;
				int j;
				for(j = 0; j < 6; j++)
				{
					if(GetBit(switchs, j))
					{
						if( j > 0)
							FlipBit(&lights[i], j - 1);
						FlipBit(&lights[i], j);
						if(j < 5)
							FlipBit(&lights[i], j + 1);
					}
				}
				if( i < 4)
					lights[i + 1] ^= switchs;
				switchs = lights[i];	
			} 
			if(lights[4] == 0)
			{
				OutputResult(t, result);
				break;
			}
		}
	}	
	return 0;
}

部分示例运行结果如下:
在这里插入图片描述
这个解法很特殊也很少见,这是用二进制数进行枚举的方法同时运用了位运算,这个解法很值得细品!!!

最近我还会有更多博文更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持,下期更精彩!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值