一、枚举算法
定义:基于逐个尝试答案的一种问题策略,它的核心思想就是枚举所有可能。
作为基本的算法,枚举没有什么固定模板或者说格式,在解题时只需找到枚举的对象、范围和判定条件,之后逐一验证枚举的对象,从而找到答案。
在实际解题过程中,真正需要思考的是一个算法优化问题,即对枚举算法的优化,有时,一个简单的优化可以减少很多运算时间,从而减少代码的复杂度。这个算法优化,才是思考的关键。
接下来通过几道题,演示枚举算法的构造,问题实现过程以及算法优化等问题。
1、完美立方
形如a^3= b^3 + c^3 + d^3的等式被称为完美立方等式。例如 12^3= 6^3 + 8^3 + 10^3 。编写一个程序,对任给的正整数N (N≤100),寻找所有的四元组(a, b, c, d),使得a^3= b^3 + c^3 + d^3,其中a,b,c,d 大于 1, 小于等于N,且 b<=c<=d。
Input一个正整数N (N≤100),Output 每行输出一个完美立方。
样例输入
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)
解题思路:很明显这道题需要枚举每一个数,只需设置for循环就可以了,然后需要思考的是优化的问题。显然,i的范围是[2,n],自然,j的范围就是[2,i)了。接下来是k,我们当然可以从2开始循环累加k,但是这样不能保证k是比j大的,其次,这样做还会增加更多的运算次数,导致代码的复杂度太大,可以这样设置k的范围[j,i),这样既避免了k比j小的问题,还减少了不必要的运算。同理,l的范围就是[k,i)了。
#include<iostream>
#include<string>
#include<cstdio>
#include<set>
#include<cmath>
#include<algorithm>
using namespace std;
int main()
{
int n;
cin >> n;
for (int i = 2; i < n; i++)
{
for (int j = 2; j < i; j++)
{
for (int k = j; k < i; k++)
{
for (int l = k; l < i; l++)
{
if (i * i * i == j * j * j + k * k * k + l * l * l)
{
cout << "Cube=" << i << " " << "Triple=(" << j << "," << k << "," << l << ")" << endl;
}
}
}
}
}
return 0;
}
2、称硬币
有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.
解题思路:本题较难,需要认真思考一下,对于每一枚硬币,我们先假设它是轻的,看这样是否符合称量结果。如果符合,问题解决,如果不符合,就假设它是重的,看是否符合称量结果,如果两种都不符合,就换下一枚硬币,这样一定能找到目标硬币。
#include<iostream>
#include<string>
#include<cstdio>
#include<set>
#include<cmath>
#include<algorithm>
using namespace std;
char Left[3][7]; //天平左边硬币
char Right[3][7]; //天平左边硬币
char Result[3][7]; //结果
bool isfake(char c, bool 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.\n";
break;
}
else if (isfake(c, false))
{
cout << c << "is the counterfeit coin and it is heavy.\n";
break;
}
}
}
return 0;
}
bool isfake(char c, bool light) //此处的bool类型,true代表轻的,false代表重的
{
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': //分析结果,'u'代表up,即右边高,说明假币在右边,如果没有在右边,说明假设不成立
if (strchr(pRight, c) == NULL)
return false;
break;
case'e': //'e'代表even
if (strchr(pLeft, c) || strchr(pRight, c))
return false;
break;
case'd': //'d'代表down
if (strchr(pLeft, c) == NULL)
return false;
break;
}
}
return true;
}
该说不说,这道题确实是挺难的难。
3、正方形检测
给你平面直角坐标系中的四个点,判断这四个点是否构成一个正方形
输入
第一行输入四个整数 xi(0<xi<10000)
第二行输入四个整数 yi(0<yi<10000)
输出
如果可以构成一个三角形,输出"it's a square"
否则输出"not a square"
解题思路:构成一个正方形的条件是四条边相等并且两条对角线也相等。因此,我们只需要枚举任意两点间的距离,从小到大排序,然后判断是否相等即可。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
#include<cstdio>
#include<set>
#include<cmath>
#include<algorithm>
using namespace std;
int main()
{
int ret = 0;
int x[10], y[10], d[10];
for (int i = 0; i < 4; i++)
{
cin >> x[i];
}
for (int i = 0; i < 4; i++)
{
cin >> y[i];
}
for (int i = 0; i < 4; i++)
{
for (int j = i + 1; j < 4; j++)
{
d[ret] = (x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]); //枚举任意两点之间的距离
ret++;
}
}
sort(d, d + 6); //将两点间距离从小到大排序
if (d[0] == d[1] && d[1] == d[2] && d[2] == d[3] && d[4] == d[5]) //四条边相等且对角线相等,则为正方形
{
cout << "It's a square" << endl;
}
else
{
cout << "Not a square" << endl;
}
return 0;
}
4、熄灯问题(大boss)
有一个由按钮组成的矩阵,其中每行有6个按钮,共5行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上边、下边、左边、右边)的灯都会改变一次。即,如果灯原来是点亮的,就会被熄灭;如果灯原来是熄灭的,则会被点亮。在矩阵角上的按钮改变3盏灯的状态;在矩阵边上的按钮改变4盏灯的状态;其他的按钮改变5盏灯的状态。
在上图中,左边矩阵中用X标记的按钮表示被按下,右边的矩阵表示灯状态的改变。对矩阵中的每盏灯设置一个初始状态。请你按按钮,直至每一盏等都熄灭。与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果。在下图中,第2行第3、5列的按钮都被按下,因此第2行、第4列的灯的状态就不改变。
请你写一个程序,确定需要按下哪些按钮,恰好使得所有的灯都熄灭。根据上面的规则,我们知道1)第2次按下同一个按钮时,将抵消第1次按下时所产生的结果。因此,每个按钮最多只需要按下一次;2)各个按钮被按下的顺序对最终的结果没有影响;3)对第1行中每盏点亮的灯,按下第2行对应的按钮,就可以熄灭第1行的全部灯。如此重复下去,可以熄灭第1、2、3、4行的全部灯。同样,按下第1、2、3、4、5列的按钮,可以熄灭前5列的灯。
输入
5行组成,每一行包括6个数字(0或1)。相邻两个数字之间用单个空格隔开。0表示灯的初始状态是熄灭的,1表示灯的初始状态是点亮的。
输出
5行组成,每一行包括6个数字(0或1)。相邻两个数字之间用单个空格隔开。其中的1表示需要把对应的按钮按下,0则表示不需要按对应的按钮。
样例输入
1 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
样例输出
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
解题思路:原本没有打算写这道题,因为实在是太难了,但是这道题非常非常经典,也有很多类似的题目,所以很有学习的必要。显然,我们不能一个一个的枚举,那是2^30次运算,恐怖如斯。可以这样想,存在一个“局部”,只要局部确定了,其他所有的就都确定了。或者说,只要一个局部的灯的状态是确定的,其他所有的灯就都能熄灭(这里教的有些晦涩,有点只可意会不可言传的意思了,建议先去看看郭炜老师的课)。那么经过观察,第一行就是这样的局部,在第一行的某种状态下,只需按动第二行的一些灯,使其熄灭,而第二行的某些灯,只需按动第三行的某些灯,以此类推,最后全部熄灭。
#include<iostream>
#include<string>
#include<cstdio>
#include<set>
#include<cmath>
#include<algorithm>
using namespace std;
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
{
if (v)
{
c |= (1 << i);
}
else
{
c &= ~(1 << i);
}
}
void Flipbit(char& c, int i)//将c的第i位取反
{
c ^= (1 << i);
}
void Outputresult(int t, char result[])//输出函数
{
for (int i = 0; i < 5; i++)//第i行
{
for (int j = 0; j < 6; j++)//第j列
{
cout << Getbit(result[i], j);
if (j < 5)
{
cout << " ";
}
}
cout << endl;
}
}
int main()
{
int T;//共有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);//读入数据
}
}
for (int n = 0; n < 64; n++)//遍历第一行的64种情况
{
char switchs = n;//储存当前行的开关状态
memcpy(lights, orilights, sizeof(orilights));//对灯进行操作是在light里进行的,所以需要一个复制
for (int i = 0; i < 5; i++)
{
result[i] = switchs;//保存第i行开关的操作方案(因为对于第i行的灯,都是已经确定的)
for (int j = 0; j < 6; j++)//根据方案修改第i行的灯
{
if (Getbit(switchs, j))//switchs的第j个位等于1表示需要按下第i行第j个按钮,等于0表示不需要按下该按钮
{
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];//第i+1行开关的操作方案由第i行灯的状态决定
}
if (lights[4] == 0)
{
Outputresult(t, result);//如果最后一行的灯全灭,说明方案成功,输出
break;
}
}
}
return 0;
}
一时半会儿一定消化不了这道题,还是要结合视频多想想。