什么是枚举算法?
在进行归纳推理时,如果逐个考察了某类事件的所有可能情况,因而得出一般结论,那么这结论是可靠的,这种归纳方法叫做 枚举法。也就是说枚举是基于 逐个尝试答案 的一种问题求解策略。
枚举法的特点
枚举算法因为要列举问题的所有可能的答案,所以它具备以下几个特点:
枚举法基本思路
采用枚举算法解题的基本思路:
例题一:完美立方
要求:请按照 a 的值,从小到大依次输出。当两个完美立方等式中a的值相同,则 b 值小的优先输出、仍相同则 c 值小的优先输出、再相同则 d 值小的先输出。
// 输入
24
// 输出
Cube = 6, Triple = (3,4,5)
Cube = 12, Triple = (6,8,10)
Cube = 18, Triple = (2,12,16)
Cube = 18, Triple = (9,12,15)
Cube = 19, Triple = (3,10,18)
Cube = 20, Triple = (7,14,17)
Cube = 24, Triple = (12,16,20)
解题思路:四重循环枚举 a、b、c、d,a 在最外层,d 在最里层,每一层都是从小到大枚举,且每个变量枚举范围如下:
代码实现:
int N;
cin >> 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)
cout << "Cube = " << d << ", Triple = (" << b << "," << c << "," << d << ")" << endl;
}
}
}
}
注意:可以通过合理控制枚举范围,来提高运算效率。
例题二:假币问题
题目描述:有 12 枚硬币。其中有 11 枚真币和 1 枚假币。假币和真币重量不同,但不知道假币比真币轻还是重。现在,用一架天平称了这些币三次,告诉你称的结果,请你找出假币并且确定假币是轻是重(数据保证一定能找出来)。
输入: 输入第一行是测试数据组数。每组数据有三行,每行表示一次称量的结果。银币标号为 A - L 。每次称量的结果用三个以空格隔开的字符串表示:天平左边放置的硬币 天平右边放置的硬币 平衡状态。其中平衡状态用 up
, down
或 even
表示,分别为 右端高
、右端低
和 平衡
。天平左右的硬币数总是相等的。
// 输入样例
1
ABCD EFGH even
ABCI EFJK up
ABIJ EFGH even
// 输出样例
K is the counterfeit coin and it is light.
解题思路:对于每一枚硬币先假设它是轻的,看这样是否符合称量结果。如果符合,问题即解决。如果不符合,就假设它是重的,看是否符合称量结果。把所有硬币都试一遍,一定能找到特殊硬币。
具体来说,设置一个 IsFake()
函数,我们先根据假币是轻的情况,来进行向下讨论。如果天平的右侧翘起 result[i][0] = 'u'
,则说明右端存在假币,因此如果判断到右侧没有假币 strchr(pRight, c) == NULL
则返回 false
。同理,当两侧平衡时,判断左右两侧只要有一侧有假币,则返回 false
;左侧翘起,判断左侧没有假币则返回 false
。这是假币为轻的情况,如果假币为重时,只需要将左右指针指向交换,就变成了前一种情况。
实现代码:
#include
#include
#include
using namespace std;
char Left[3][7]; // 天平坐边硬币
char Right[3][7]; // 天平右端硬币
char result[3][7]; // 结果
bool IsFake(char c, bool light) // c 为假币,同时 light 为 true 表示假设假币为轻
{
// 三次称量结果不矛盾,假设成立
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." << endl;
break;
}
else if (IsFake(c, false))
{
cout << c << " is the counterfeit coin and it is heavy." << endl;
break;
}
}
}
return 0;
}
strchr() 方法
- 库:c 标准库
- 语法: char *strchr(const char *str, char c)
- 功能:获取字符串 str 中字符 c 第一次出现的位置。
- 返回值:返回在字符串 str 中第一次出现字符 c 的位置,如果未找到该字符则返回 NULL。
例题三:熄灯问题
题目概述:有一个由按钮组成的矩阵,其中每行有 6 个按钮,共 5 行。每个按钮的位置上有一盏灯,当按下一个按钮后, 该按钮以及周围位置(上边,下边,左边,右边)的灯都会改变状态:
Ⅰ. 如果灯原来是点亮的,就会被熄灭。
Ⅱ. 如果灯原来是熄灭的,则会被点亮。
Ⅲ. 与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果。
要求:给定矩阵中每盏灯的初始状态,求一种按按钮方案,使得所有的灯都熄灭。
输入: 第一行是一个正整数 N,表示需要解决的案例数。每个案例由 5 行组成, 每一行包括 6 个数字,这些数字以空格隔开, 可以是 0 或 1。0
表示灯的初始状态是 熄灭
的,1
表示灯的初始状态是 点亮
的。
输出:对每个案例,首先输出一行:输出字符串 PUZZLE #m
, 其中 m 是该案例的序号。接着按照该案例的输入格式输出 5 行,1 表示需要把对应的按钮按下,0 表示不需要按对应的按钮。每个数字以一个空格隔开。
// 输入
2
0 1 1 0 1 0
1 0 0 1 1 1
0 0 1 0 0 1
1 0 0 1 0 1
0 1 1 1 0 0
0 0 1 0 1 0
1 0 1 0 1 1
0 0 1 0 1 1
1 0 1 1 0 0
0 1 0 1 0 0
// 输出
PUZZLE #1
1 0 1 0 0 1
1 1 0 1 0 1
0 0 1 0 1 1
1 0 0 1 0 0
0 1 0 0 0 0
PUZZLE #2
1 0 0 1 1 1
1 1 0 0 0 0
0 0 0 1 0 0
1 1 0 1 0 1
1 0 1 1 0 1
解题思路:
2^30
,会导致超时。那么怎么减少枚举状态数呢?2^6 = 64
。位运算相关文章: 位运算(&、|、^、~、>>、<<);【技巧总结】位运算装逼指南
实现代码:这题也太难为我了。。。勉强看懂,但是让我自己实现的话,
#include
#include
#include
using namespace std;
char oriLights[5]; // 初始灯矩阵
char lights[5]; // 变化过程中的灯矩阵
char result[5]; // 结果开关矩阵
// 获取第 i 位
int GetBit(char c, int i)
{
// 101001 获取第 4 位
// 1. 000010 & 000001 = 000000
// 2. 即获取到 101001 的第四位 0
return (c >> i) & 1;
}
// 设置第 i 位为 v (0 或 1)
void SetBit(char &c, int i, int v)
{
if (v)
// 设置第 i 位为 1
// c: 101001 i: 4 1<
// 101001 | 010000 = 111001 成功将第 i 位设置为 1
c |= (1 << i);
else
// 设置第 i 位为 0
// c: 101001 i: 4 1<
// 101001 & 101111 = 101001 成功将第 i 位设置为 0
c &= ~(1 << i);
}
// 反转第 i 位
void FlipBit(char &c, int i)
{
// 反转第 i 位
// c: 101001 i: 4 1<
// 101001 ^ 010000 = 111001 成功将第 i 位反转
c ^= (1 << i);
}
void OutputResult(int t, char result[])
{
cout << "PUZZLE #" << t << endl;
for (int i = 0; i < 5; ++i)
{
for (int j = 0; j < 6; ++j)
{
cout << GetBit(result[i], j);
if (j < 5)
cout << " ";
}
cout << endl;
}
}
int main()
{
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;
SetBit(oriLights[i], j, s);
}
}
// 枚举第一行开关所有可能的 64 种状态
for (int n = 0; n < 64; ++n)
{
int switches = n; // 当前行的开关状态
memcpy(lights, oriLights, sizeof(oriLights));
for (int i = 0; i < 5; ++i)
{
result[i] = switches; // 第 i 行的开关状态
for (int j = 0; j < 6; ++j)
{
// (i, j) 按下时 进行处理
if (GetBit(switches, j))
{
if (j > 0)
FlipBit(lights[i], j - 1); // 反转左侧灯
FlipBit(lights[i], j); // 反转当前开关位置灯
if (j < 5)
FlipBit(lights[i], j + 1); // 反转右侧灯
}
}
// 处理 i+1 行
if (i < 4)
lights[i + 1] ^= switches; // 改下一行的灯
switches = lights[i];
}
if (lights[4] == 0)
{
OutputResult(t, result);
break;
}
}
}
return 0;
}