Reference:https://www.icourse163.org/learn/PKU-1001894005?tid=1450413466#/learn/content
基于逐个尝试答案的一种问题求解策略。
使用枚举方法,最好可以通过一些明显的特征,减少枚举数量。
提高枚举效率的方法就是,跳过不必要的测试值。
例如:求小于 N 的最大素数。
找不到一个数学公式,使得根据 N 就可以计算出这个素数,所以只好使用枚举方法。
完美立方
通过条件减小枚举范围。
形如 a3 = b3 + c3 + d3 的等式称为 完美立方等式。
Q:对任给的正整数 N(N<=100),寻找所有的四元组(a、b、c、d),使得 a3 = b3 + c3 + d3 ,其中 a、b、c、d 大于 1,小于等于 N,且 b<=c<=d 。
```C
#include <stdio.h>
#include <Windows.h>
// 微软认为 scanf() 不安全,所以在 VS 中添加以下两条预处理命令,避免因为scanf报错。
#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable : 4996)
int main()
{
int N;
bool flag = false;
printf("please input N(0<N<=100):");
while (true) {
scanf("%d", &N);
if (N > 0 && N <= 100) {
break;
}
}
/* 缩小枚举范围,提高效率
因为 a3 = b3 + c3 + d3
所以 1<b,c,d<a
又因为 1<a,b,c,d<N
所以 1<a<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);
flag = true;
}
}
}
}
}
// 找不到结果时,提示没有符合的数据
if (flag == false) {
printf("未找到符合条件的数据。\n");
}
Sleep(50000);
return 0;
}
```生理周期
逐步查找符合条件的值,跳过不必要的测试值。
人有体力、情商、智商的高峰日子,它们分别每隔23天、28天和33天出现一次。对于每个人,我们想知道何时三个高峰落在同一天。给定三个高峰出现的日子p,e和i(不一定是第一次高峰出现的日子), 再给定另一个指定的日子d,你的任务是输出日子d之后,下一次三个高峰落在同一天的日 子(用距离d的天数表示)。例如:给定日子为10,下次出现三个高峰同一天的日子是12,则输出2。
Q:输入四个整数:p, e, i和d。 p, e, i分别表示体力、情感和智力高峰出现的日子。d是给定的日子,可能小于p, e或 i。所有给定日子是非负的并且小于或等于365,所求的日子小于或等于21252。
```C
#include <stdio.h>
#include <Windows.h>
#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable : 4996)
int main()
{
int p, e, i, d;
printf("请输入依次输入体力、情感、智力的高峰日和指定的日子(以空格隔开):");
while (1) {
scanf("%d %d %d %d", &p, &e, &i, &d);
if ((p >= 0 && p <= 365) && (e >= 0 && e <= 365) && (i >= 0 && i <= 365) && (0 <= d && d <= 365)) {
break;
}
}
/* 从d+1天开始,一直试到第21252 天,对其中每个日期k,看是否满足
通过for循环可以跳过不必要的测试值,较快的锁定k的值
*/
int k = d + 1;
// 当第一个for循环结束时,(k-p)%23==0,k代表体力高峰日
for (; (k - p) % 23; k++);
// 当第二个for循环结束时,(k-e)%28==0,k代表情商高峰日,又因为k的步进为23,所以保证了k仍是体力高峰日
for (; (k - e) % 28; k += 23);
// 当第三个for循环结束时,(k-i)%33==0,k代表智商高峰日,又因为k的步进为23*28(23和28的最小公倍数),所以保证了k仍是体力和情商高峰日。至此,k为三个生理周期的高峰日。
for (; (k - i) % 33; k += 23 * 28);
printf("the next triple peak occurs in %d days.", k-d);
Sleep(50000);
return 0;
}
```称硬币(假币问题)
当结果是少数有限种情况时,假设其中一种情况,带入条件进行验证。
有12枚硬币。其中有11枚真币和1枚假币。假币和真币重量不同,但不知道假币比真币轻还是重。
Q:用一架天平称了这些币三次,告诉你称的结果,请你找出假币并且确定假币是轻是重(数据保证一定能找出来)。
```C
#include <stdio.h>
#include <Windows.h>
#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable : 4996)
/* 设三个二维数组,分别用来表示天平左边、右边、结果,三次称量对应的情况。
因为共有12枚硬币,且每次称量两边数量必须一样,所以每边最多能放6个。
*/
char left[3][7];
char right[3][7];
char result[3][7];
// 假设传入的 c 是假币。且 light 为真时,假币较轻;反之,假币较重。
bool isFake(char c, bool light);
int main()
{
printf("请依次输入(大写)左边天平的硬币标号、右边天平的硬币标号、称量结果。\n");
for (int i = 0; i < 3; i++) {
printf("\n第 %d 次称量:", i);
scanf("%s", left[i]);
scanf("%s", right[i]);
scanf("%s", result[i]);
}
for (char c = 'A'; c <= 'L'; c++) {
if (isFake(c, true)) {
printf("%c is the counterfelt coin and it's light.\n", c);
break;
}
else if (isFake(c, false)) {
printf("%c is the counterfelt coin and it's heavy.\n", c);
break;
}
}
Sleep(50000);
return 0;
}
bool isFake(char c, bool light) {
// 将假设带入三次称量结果中进行验证
for (int i = 0; i < 3; i++) {
char* pleft;
char* pright;
if (light) {
pleft = left[i];
pright = right[i];
}
else {
// 当假币较重时,情况相反
pleft = right[i];
pright = left[i];
}
// 假设不成立时返回false
switch (result[i][0]) {
case 'U':
if (strchr(pright, c) == NULL) return false; // C 库函数 char* strchr(const char* str, int c) 在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。
break;
case 'E':
if (strchr(pleft, c) || strchr(pright, c)) return false;
break;
case 'D':
if (strchr(pleft, c == NULL)) return false;
break;
}
}
// 假设成立返回true
return true;
}
```熄灯问题
通过一个有代表性的局部算法,该局部状态一旦确定,其他部分状态也确定,所以枚举该局部的所有状态即可跳过其他部分不必要的测试值。
有一个由按钮组成的矩阵, 其中每行有6个按钮, 共5行。每个按钮的位置上有一盏灯,当按下一个按钮后, 该按钮以及周围位置(上边, 下边, 左边, 右边)的灯都会改变状态(亮--->灭;灭---> 亮)。
Q:给定矩阵中每盏灯的初始状态,求一种按按钮方案,使得所有的灯都熄灭。
分析:同一个按钮按两次会抵消第一次按下的效果,相当于没按,所以,按钮要么按一次,要么不按;
因为每个按钮最多按一次,所以按下按钮的顺序对最终结果没有影响;
锁定局部为第一行,因为要熄灭第一行所有的灯,则第二行只有唯一的按按钮方案;
同理,依次熄灭第一、二、三、四行所有的灯,此时只要最后一行也全部熄灭,则存在满足条件的按钮方案。
这里通过这种方法,可以将枚举数量从 25*6 减小到 26 。
```C
#include <stdio.h>
#include <Windows.h>
#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable : 4996)
int main()
{
int N;
printf("请输入测试次数:");
scanf("%d", &N);
int n = N;
while (N--) {
// 用二维数组表示每盏灯的初始状态。0 表示熄灭;1 表示点亮
int status[5][6];
// 用二维数组表示按钮。0 表示不按的按钮,1 表示按下的按钮。
int button[5][6] = { 0 };
// 某一行按按钮的方案
int switchs[6];
// 存储按过按钮后每盏灯的状态
int lights[5][6];
printf("\n请输入第 %d 次测试的数据。\n", n-N);
// 接收测试灯的初始状态
for (int i = 0; i < 5; i++) {
printf("第 %d 行灯的初始状态(以空格隔开):", i+1);
scanf("%d %d %d %d %d %d", &status[i][0], &status[i][1], &status[i][2], &status[i][3], &status[i][4], &status[i][5]);
lights[i][0] = status[i][0];
lights[i][1] = status[i][1];
lights[i][2] = status[i][2];
lights[i][3] = status[i][3];
lights[i][4] = status[i][4];
lights[i][5] = status[i][5];
}
// 对第一行灯的按按钮方案进行枚举
for (int k = 0; k < 2 * 2 * 2 * 2 * 2 * 2; k++) {
// 将 k 分解为二进制赋给 switchs
switchs[0] = (k>=32 ? 1 : 0);
switchs[1] = (k%32>=16 ? 1 : 0);
switchs[2] = (k%16>=8 ? 1 : 0);
switchs[3] = (k%8>=4 ? 1 : 0);
switchs[4] = (k%4>=2 ? 1 : 0);
switchs[5] = (k%2==1 ? 1 : 0);
for (int i = 0; i < 5; i++) {
if (i > 0) {
for (int j = 0; j < 6; j++) {
// 上一行灯的状态决定了本行要按的按钮
switchs[j] = lights[i-1][j];
}
}
for (int j = 0; j < 6; j++) {
if (switchs[j] == 1) {
button[i][j] = 1; // 按过的按钮存入button
lights[i][j] = lights[i][j] == 1 ? 0 : 1; // 更改本位的灯的状态
// 更改上下两行的灯的状态
if (i > 0) {
lights[i - 1][j] = lights[i - 1][j] == 1 ? 0 : 1;
}
if (i < 4) {
lights[i + 1][j] = lights[i + 1][j] == 1 ? 0 : 1;
}
// 更改左右两边的灯的状态
if (j > 0) {
lights[i][j - 1] = lights[i][j - 1] == 1 ? 0 : 1;
}
if (j < 5) {
lights[i][j + 1] = lights[i][j + 1] == 1 ? 0 : 1;
}
}
}
}
if (lights[4][0] + lights[4][1] + lights[4][2] + lights[4][3] + lights[4][4] + lights[4][5] == 0) {
printf("PUZZLE # %d\n", n-N);
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
printf("%d ", button[i][j]);
}
printf("\n");
}
break;
}
else {
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
button[i][j] = 0;
lights[i][j] = status[i][j];
}
}
}
}
}
Sleep(50000);
return 0;
}
```上述算法已经可以实现题目的要求,但是使用 int 类型的二维数组来实现太过浪费内存。
在 64位 计算机中,一个 int 类型的变量占用 4Byte 空间,可以通过 short、 char 等占用空间更小的类型来实现上述算法。
在本题中,灯只有两种状态:亮、灭;按钮一直有两种状态:按、不按。所以更节约内存的方法是,直接使用 bit 位来表示灯或按钮。
```C
#include <stdio.h>
#include <iostream>
#include <Windows.h>
#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable : 4996)
int getBit(char c, int i) {
return (c >> i) & 1;
}
void setBit(char& c, int i, int v) {
if (v) {
c |= (1 << i); // 设 1 时,其余位与 0 相或,保证其余位不变
}
else {
c &= ~(1 << i); // 设 0 时,其余位与 1 相与,保证其余位不变
}
}
void flip(char& c, int i) {
c ^= (1 << i); // 取反时,其余位与 0 异或,保证其余位不变,与 1 异或则取反。这里表示改变灯的状态
}
int main()
{
int N;
printf("请输入测试次数:");
scanf("%d", &N);
int n = N;
while (N--) {
// 用char类型的一维数组中的比特位表示每盏灯的初始状态。0 表示熄灭;1 表示点亮
char status[5];
// 用char类型的一维数组表示按钮。0 表示不按的按钮,1 表示按下的按钮。
char button[5] = { 0 };
// 某一行按按钮的方案
char switchs;
// 存储按过按钮后每盏灯的状态
char lights[5];
// 将灯的状态全部初始化为 0 ,切记一定要初始化。否则,char数组未赋值前,系统会令数组指针随机的指向任意一块可利用的地址。而这块地址可能存有失效的数据,导致最后 lights[4] 无法等于 0。
memset(status, 0, sizeof(status));
// C 库函数 void *memset(void *str, int c, size_t n) 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
printf("请输入第 %d 次测试的数据。\n", n-N);
// 接收测试灯的初始状态
for (int i = 0; i < 5; i++) {
printf("第 %d 行灯的初始状态(以空格隔开):", i+1);
int k;
for (int j = 0; j < 6; j++) {
//scanf("%d ", &k); 使用 scanf 函数时,可能第一次输入的值无法读入
std::cin >> k;
setBit(status[i], j, k);
printf("%d %d\n",i,j);
}
lights[i] = status[i];
}
// 对第一行灯的按按钮方案进行枚举
for (int k = 0; k < 2 * 2 * 2 * 2 * 2 * 2; k++) {
switchs = k;
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
if (getBit(switchs,j)) {
setBit(button[i],j, 1); // 按过的按钮存入button
flip(lights[i], j); // 更改本位的灯的状态
// 更改上下两行的灯的状态
if (i > 0) {
flip(lights[i-1], j);
}
if (i < 4) {
flip(lights[i+1], j);
}
// 更改左右两边的灯的状态
if (j > 0) {
flip(lights[i], j-1);
}
if (j < 5) {
flip(lights[i], j+1);
}
}
}
// 第二行开始,本行灯的状态决定了下一行要按的按钮
switchs = lights[i];
}
if (lights[4] == 0) {
printf("PUZZLE # %d\n", n-N);
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
printf("%d ", getBit(button[i], j));
}
printf("\n");
}
break;
}
else {
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
setBit(button[i],j,0);
setBit(lights[i],j, getBit(status[i],j));
}
}
}
}
}
Sleep(50000);
return 0;
}
```


京公网安备 11010502036488号