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;
}
```