题目描述
原题连接:95. 费解的开关 - AcWing题库
解答
这道题确实有些许费解,至少对于第一次见到类似题目的人大概率会摸不着头脑,虽然官网里的解答已经十分详尽了,但个人认为多少还是有些“不说人话”。
实际上这个题目并没有绕多大的弯,首先我们头脑风暴一下这个题目的解决方法,容易想到应该使用递推,即根据每行灯泡之间的按钮变化关联确定状态转移方式。具体是怎样的?假设现在有这样两行灯泡:
00100
11001
那么我们应该怎么按灯?不妨自上往下看,很明显,第二行的灯泡按键会影响到第一行,先别管第二行及其之后的灯泡,关注第一行,想要让第一行的灯全亮的话,很明显只需要按下第二行的y轴对应的灯泡就行,所以以此类推,后面的几行灯泡也是如此,根据这种做法我们其实也能看出个特性,即“我们的按法其实早就被第一行的灯泡亮灭情况决定了”(请仔细理解这句话)最后检查一下第五行的灯泡是否是全亮的就能确定这组灯泡是否能被全部按亮了,且能够获得其最小的操作步数了......吗?
恭喜,如果你感到奇怪或者已经看出来了的话说明你的敏感度不错。这个做法的漏洞其实很明显,那就是——“为什么我们不按第一行?”,但这也是大部分人没有理解的地方。
是的,我们可以按第一行,而且是应该按第一行,为什么?因为实际上有可能按了第一行的灯泡后,我们的答案可能才会是最佳的,这里就不举例了,有兴趣者请自行研究。但我们并不知道到底哪个才是最佳答案的按法,所以需要做一次暴力遍历,次数需要2^5即32次,下面是带详细注释的ac代码,时间复杂度为O(n):
#include <iostream>
#include <cstring>
using namespace std;
const int N = 6;
int dx[N] = { -1,0,1,0,0 }, dy[N] = { 0,-1,0,1,0 }; //左上右下中
char m[N][N], backup[N][N];
//这种变换关联数据的方法是比较常见的
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;
m[a][b] ^= 1;
}
}
int main() {
int n;
cin >> n;
while (n--) {
for (int i = 0; i < 5; ++i) {
cin >> m[i];
}
//事先备份原地图
memcpy(backup, m, sizeof(m));
int ans = 0x7FFFFFFF;
//模拟第一行的所有情况,从中选出操作最小值作为答案
for (int op = 0; op < 32; ++op) {
int step = 0;
//先根据当前op操作第一行,比如如果op=1,即00001,那么就是第一行第一个开关要按一下
for (int i = 0; i < 5; ++i) {
if ((op >> i) & 1) {
turn(0, i);
++step;
}
}
//然后之后的每行要根据前一行的灯亮灭情况进行按键操作
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 5; ++j) {
if (m[i][j] == '0') {
turn(i + 1, j);
++step;
}
}
}
bool OK = true;
//最后看看第五行有没有0就行了,有的话就说明这次操作不行,没有结果
for (int i = 0; i < 5; ++i) {
if (m[4][i] == '0') {
OK = false;
break;
}
}
if (OK) ans = min(ans, step);
//记得还原地图
memcpy(m, backup, sizeof(backup));
}
if (ans > 6) cout << -1 << '\n';
else cout << ans << '\n';
}
return 0;
}
总结
很多递推问题都会对未知量进行一个暴力的遍历,这一点是需要注意的,但递推问题的难度主要还是在于发现递推的状态转移关系,想要提高只能Make more efforts~,理解了的话不妨再来一题锻炼锻炼:96. 奇怪的汉诺塔 - AcWing题库