【蓝桥杯备赛笔记 02】 费解的开关(模拟实现)
我好菜,搞了一天,看了题解才知道这题解题的巧妙点在哪,不过好歹是理解了这题的解题思路。。。
题目链接: 95. 费解的开关
解题思路
对于这一题,有三个限制:
- 对某一开关操作会导致其上下左右四个方向的灯跟随当前位置一起发生亮灭改变
- 进行完操作后所有的灯必须全亮(题目要求)
- 按灯的次数要尽量的保持少
对于这几个要求,可以得出几个结论:
- 一个位置顶多只会操作一次。因为如果操作两次的话,相当于不操作,必然是不满足最优解。
- 一套方案中,操作的顺序无关紧要,这一个略加思索便可得知
- 最重要的性质:确定第一行其实就确定了所有的操作
下面给出一个详细的解答。
为什么确定第一行其实就确定了所有的操作?
当我们确定了第一行,就代表第一行不能动了,而操作第二行会导致第一行的状态发生改变,所以我们得率先操作完第二行,保证第一行的值为我们题目的需求。这样根据第二条结论,不能改变第二行的值了,所以我们只能通过对第三行操作才能改变第二行原本不符合条件的值。同理下面的所有操作其实都在我们确定第一行的操作后确定了,我们接下来只需要判断一下最后一行的值是否是我们需要的值,如果不适合或者总共的操作次数大于6次输出-1,否则再与上一次成功的操作数做对比,输出较小者。
例:(这里借鉴https://blog.csdn.net/qq_41021816/article/details/81810059的示例)
首先我们要枚举第一行灯开关的所有情况,那么如果要改变第一行的灯的状态,那么就只能更改第二行的位于该灯下面的那个开关来改变。
如果我们固定了第一行,那么为了将全部都变成绿色,就必须利用第二行。例如,(1,1)(1,1)是红色,为了让它变成绿色,就必须更改(2,1)(2,1)。为了让(1,4)(1,4)变成绿色,就必须更改(2,4)(2,4)。
更改后图形如下:
那么我们再固定第二行,利用第三行来更改它(就像用第一行来更改第二行一样),就变成了
同理,更改第三行
再更改第四行
这时我们发现,最后还有一个灯是关着的,所以,这说明第一行的灯如果是这样的情况就无法成立
那么就继续枚举第一行的点击方式,再继续按照刚才的方法,判断能否点完即可。
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 6;
//g : 用以存储元数据 、
//backup: 用以存储拷贝的数据
char g[N][N], backup[N][N];
//偏移量
int dx[5] = {-1, 0, 1, 0, 0}, dy[5] = {0, 1, 0, -1, 0};
// turn函数用以模拟坐标中 的g[x][y]被按下后的反应
void turn(int x, int y)
{
for (int i = 0; i < 5; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= 5 || b < 0 || b >= 5) continue; // 在边界外,直接忽略即可
g[a][b] ^= 1;
}
}
int main()
{
int T;
cin >> T;
while (T -- )
{
//接收第一行
for (int i = 0; i < 5; i ++ ) cin >> g[i];
//res为一个理想中的最大值,事实上大于6即可,因为>6就输出-1
int res = 10;
//对于每一种第一行的可能操作进行枚举
for (int op = 0; op < 32; op ++ )
{
//拷贝一份原数组的备份
memcpy(backup, g, sizeof g);
int step = 0;
//根据第一行的各位置的值对第二行进行操作
for (int i = 0; i < 5; i ++ )
if (op >> i & 1)
{
step ++ ;
turn(0, i);
}
//对剩下的三行进行同样操作
for (int i = 0; i < 4; i ++ )
for (int j = 0; j < 5; j ++)
if (g[i][j] == '0')
{
step ++ ;
turn(i + 1, j);
}
//下面的操作是对最后一行的判断,如果全亮说明该选择可行,计入res
bool dark = false;
for (int i = 0; i < 5; i ++ )
if (g[4][i] == '0')
{
dark = true;
break;
}
//全亮且值比上一次的res小则替代。
if (!dark) res = min(res, step);
//拷贝回g ,选择完成。
memcpy(g, backup, sizeof g);
}
if (res > 6) res = -1;
cout << res << endl;
}
return 0;
}
对于代码的注释:
请在浏览一遍代码以及我的注释之后再看这一部分
关于 turn函数
为了模拟开关按下的操作而设计,我这里定义了两个数组dx和dy,用以分别表示按下(x,y)按键后的x和y的偏移量。
代码第22行采用按位异或1的方式来实现0和1的互相转换。1 ^ 1 =>0 , 0 ^ 1 => 1
关于枚举第一行操作的方式
枚举第一行,我们可以用一个5位的二进制来表示第一行的所有操作,即十进制的op = 0~31来表示5个按键的不同操作,例如op = 00001代表仅对第一个开关进行操作。
代码第47行:op >> i & 1 可以取出op的第i位二进制数值。可以自己用纸笔计算验证