枚举
算法思想
基于逐个尝试答案的一种问题求解策略
优化策略
枚举虽然是一种逐个尝试的问题求解策略,但是也是可以进行优化的,有些可能的答案或者可能的猜测在进行稍加判断后,就可以直接排除掉,从而避免代入问题进行求解,进一步节省时间。具体的优化方法,往往要根据具体问题具体分析,没有统一的标准。
基本例题
case 1 :完美立方
问题描述:形如的等式被称为完美立方等式。例如:。编写一个程序,对任意给定的整数(,寻找所有的四元组,使得,其中大于1,小于等于,且
- 解题(优化)思路
范围的确定,从而避免了将所有可能全部尝试的,提高算法效率。
示例代码
#include <iostream>
using namespace std;
int main() {
int N;
cin>>N;
int a,b,c,d;
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){
cout<<"Cube = "<<a<<", Triple = ("<<b<<","<<c<<","<<d<<")"<<endl;
}
}
}
}
}
return 0;
}
case 2 :生理周期
问题描述:人有体力、情商、智商的高峰日子,它们分别每隔23天、28天和33天出现一次。对于每个人,我们想知道何时这三个高峰落在同一天。给定三个高峰出现的日子(不一定是第一次高峰出现的日子),再给定另一个指定的日子,你的任务是输出日子之后,下一次三个高峰落在同一天的日子(用距离之后的天数表示)。例如:给定日子为10,下一次出现三个高峰同一天的日子是12,则输出2。
- 解题(优化)思路
一般思路即是将所有日子进行判断,看是否是体力、情商、智商的三高峰,这种做法需要将所有的日子遍历一遍进行判断,时间成本太高。可以采用如下方法减少时间成本:从给定日子 开始,找到第一个体力高峰,然后再以体力高峰的时间间隔 递增,判断该天是否也是情商高峰,直到找到体力、情商的双高峰,之后以体力、情商高峰双高峰的时间间隔 递增,判断该天是否也为智商高峰,直到找到体力、情商、智商的三高峰,即为问题答案。该方法减少了对大量不可能日子的判断,从而有效的额提高了算法效率。
示例代码
#include <iostream>
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 = 0;
for(k = d+1; (k-p)%23 != 0; ++k);
cout<<k<<endl;
for(; (k-e)%28 != 0; k+=23);
for(; (k-i)%33 != 0; k+=23*28);
cout<<"Case "<<caseNo<<": the next triple peak occurs in "<<k-d<<" days"<<endl;
}
return 0;
}
case 3:称硬币
问题描述:有 枚硬币。其中有 枚真币和 枚假币。假币和真币的重量不同,但不知道假币比真币轻还是重。现在,用一架天平称了这些币三次,告诉你称量结果,请你找出假币并且确定假币是轻是重(数据保证一定能找出来)。
- 解题(优化)思路
对于每一枚硬币先假设它是轻的,看这样是否符合称量结果。如果符合,问题即解决。如果不符合,就假设它是重的,看是否符合称量结果。把所有的硬币都试一遍,一定能找到特殊硬币。
示例代码
#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); // light 为真表示假设硬币为轻,否则表示假设硬币为重
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;
}
}
}
return 0;
}
// 判断是否为假币
bool IsFake(char c, bool light) {
// 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;
}
case 4:熄灯问题
问题描述:有一个由按钮组成的矩阵,其中每行有6个按钮,共5行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上边、下边、左边、右边)的灯都会改变一次。即,如果灯原来是点亮的,就会被熄灭;如果灯原来是熄灭的,则会被点亮。在矩阵角上的按钮改变3盏灯的状态;在矩阵边上的按钮改变4盏灯的状态;其他的按钮改变5盏灯的状态。
在上图中,左边矩阵中用X标记的按钮表示被按下,右边的矩阵表示灯状态的改变。对矩阵中的每盏灯设置一个初始状态。请你按按钮,直至每一盏等都熄灭。与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果。在下图中,第2行第3、5列的按钮都被按下,因此第2行、第4列的灯的状态就不改变。
请你写一个程序,确定需要按下哪些按钮,恰好使得所有的灯都熄灭
- 关键点
- 第2次按下同一个按钮时,将抵消第1次按下时所产生的结果。因此,每个按钮最多只需要按下一次。
- 各个按钮被按下的顺序对最终的结果没有影响。
- 对第1行中每盏点亮的灯,按下第2行对应的按钮,就可以熄灭第1行的全部灯。如此重复下去,可以熄灭第1、2、3、4行的全部灯。同样,按下第1、2、3、4、5列的按钮,可以熄灭前5列的灯。
- 解题(优化)思路
- 一般思路:每一个灯有 种状态,开或者关,分别用 、 表示,共有 盏灯,因此共有 种情况,分别对每一种情况进行判断即可,有可能会超时,且效率低下。
- 优化思路:如果存在某个局部,一旦这个局部的状态被确定,那么剩余其他部分的状态只能是确定的一种,或者不多的 种,那么就只需枚举这个局部的状态即可。
- 经过观察,本题的局部就是第1行
- 因为第1行的各开关状态确定的情况下,这些开关作用过后,将导致第1行某些灯是亮的,某些灯是灭的。
- 要熄灭第1行某个亮着的灯(假设位于第 列),那么唯一的办法就是要按下第2行第 列的开关(因为第1行的开关已经用过了。而第3行及其以后的开关不会影响到第1行)
- 为了使第1行的灯全部熄灭,第2行的合理开关状态就是唯一的。
示例代码
#include <iostream>
#include <memory>
#include <cstring>
using namespace std;
char oriLights[5]; // 灯的原始状态
char lights[5]; // 变化之后灯的状态
char result[5]; // 结果
// 一个字符(char) 有 8 位(bit)
// 取字符 c 的第 i 个比特
int GetBit(char c, int i) {
return (c>>i) & 1; // 移位 + 与运算
}
// 设置字符 c 的第 i 个比特为 v
void SetBit(char & c, int i, int v) {
if (v) {
c |= ( 1 << i ); // 移位 + 或运算
}
else {
c &= ~(1 << i); // 移位 + 与预算
}
}
// 将字符 c 的第 i 位翻转
void FlipBit(char & c, int 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++) {
for (int i=0; i<5; i++) {
for (int j=0; j<6; j++) {
int s;
cin >> s;
SetBit(oriLights[i], j, s);
}
cout<<"oriLights : "<<oriLights[i]<<endl; // 存储灯的原始状态
}
for (int n=0; n<64; n++) { // n 从 1~63,一个值代表一种状态(每一行的)
int switchs = n;
memcpy(lights, oriLights, sizeof(oriLights));
for (int i=0; i<5; i++) {
result[i] = switchs;
for (int 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;
}
- 代码细节分析
- 考察到题目中所需的二维数组较小,且元素非0即1,采用 类型的一维数组进行存储,一个 类型有8个 bit,而题中仅仅用到了其中的6个,结合 位运算,可以降低时间复杂度和空间复杂度。
- 采用二进制数枚举,如果有若干个比特(位),每个比特有 两种取值,这若干个比特所对应的所有组合都要枚举到,就可以采用一个整数来进行。原理如下:个比特,可采用整数 进行枚举。~ 之间的每个整数对应一种组合,结合位运算实现。
总结与反思
虽然枚举是一种基于逐个尝试答案的一种问题求解策略,但是这并不意味着我们无法对其进行优化,有一些问题在进行稍加判断后就可以缩小尝试的范围,进而避免了一些不可能情况的尝试,可以极大的提高算法效率。
最后有需要工程文件的朋友可以在评论里说明(记得指明邮箱),小编看到后会第一时间发送到指定邮箱。文章如有不严谨之处,欢迎大家指正!!!