Description
小张是一个密室逃脱爱好者,在密室逃脱的游戏中,你需要解开一系列谜题最终拿到出门的密码。现在小张需要打开一个藏有线索的箱子,但箱子上有下图所示的密码锁。
每个点是一个按钮,每个按钮里面有一个小灯。如上图,红色代表灯亮,白色代表灯灭。每当按下按钮,此按钮的灯以及其上下左右四个方向按钮的灯状态会改变(如果原来灯亮则灯灭,如果原来灯灭则灯亮)。如果小张通过按按钮将灯全部熄灭则能可以打开箱子。
对于这个密码锁,我们可以先按下左上角的按钮,密码锁状态变为下图。
再按下右下角的按钮,密码锁状态变为下图。
最后按下中间的按钮,灯全部熄灭。
现在小张给你一些密码锁的状态,请你告诉他最少按几次按钮能够把灯全部熄灭。
Input
第一行两个整数n,m(1<=n,m<=16)
接下来n行,每行一个长度为的01字符串,0表示灯初始状态灭,1表示灯初始状态亮。第一行两个整数。
Output
一行一个整数,表示最少按几次按钮可以把灯全部熄灭。
Notes
第一个样例见题目描述,第二个样例按左上和右下两个按钮。
测试用例保证一定有解。
测试输入 期待的输出 时间限制 内存限制 额外进程 测试用例1
- 3 3↵
- 100↵
- 010↵
- 001↵
- 3↵
1秒 64M 0 测试用例二
- 2 3↵
- 111↵
- 111↵
- 2↵
1秒 64M 0
一、第一印象
刚看到这题时,我是毫无思路,不知道根据什么策略按按钮,想暴力枚举,不过2^(16*16)的运算数实在太恐怖……思考了很久,也只得到两个结论:
1.每个按钮要么按一次要么不按,按多了效果抵消了没有意义;
2.按按钮的顺序不重要,关键是按了哪些按钮,同样一些按钮根据不同的顺序按效果是相同的。
二、初识DFS
于是我又来到了讨论区膜拜大佬们的解法,了解到了dfs算法,上博客里查了相关资料。以下是我对于dfs的一些看法:
1.dfs学名深度优先搜索,别名<不撞南墙不回头>,顺着当前的路一直搜索下去,直到无解或得到解;
2.dfs本质就是递归,需要设置递归边界,然后穷尽所有情况,在进行下一步搜索的过程中不断调用自身,直到到达递归边界,之后再进行回溯,注意dfs在回溯前一定要复原。
三、再看题目
根据题目含义,在第一行所有灯的开关情况给定时,我们在接下来的每一行中都只需将上面行的灯关闭,直至最后一行,如果我们将倒数第二行的灯都关闭之后,最后一行灯全灭,则有解,否则无解。
所以我们只需要穷举第一行所有灯的开关情况,接着判断每种情况下最终是否有解,在有解时求得最小操作数即可。
因此,我们可以用dfs算法去模拟第一行所有灯的开关情况。对于每一个按钮,我们都可以选择按或不按,紧接着去搜索之后的情况,即调用dfs自身,当达到递归边界,即按到最后一个按钮的时候,我们可以对下面各行的灯逐行操作,根据最后一行的情况确定是否有解以及最小操作数,之后我们进行回溯,继续搜索别的情况。这样的话,我们的最大操作数基本为2^16*m*n,可以接受。
四、代码实现
思路有了,在代码实现上还有许多细节需要注意:
1.在实行逐行灭灯操作时,应选择用一个b数组来记录,不应用原来的a数组,否则a数组难以复原,在这里b数组可以是全局变量也可以定义在函数内部,结果均正确,这里也可看到递归内部具体过程虽复杂,但还是逐个实现的;
2.输入a数组时,可用getchar逐个字符读取,再转化为整数存入a数组;
3.代码要高度封装化,我自己是写了一个change函数用于改变灯光的明灭,用press函数实现按按钮操作,用dfs模拟对第一行灯光的操作,用calculate函数计算操作数。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE 17
int m,n,a[SIZE][SIZE],min=256;
void change(int *elem);
void press(int i,int j,int array[SIZE][SIZE]);
int calculate();
void dfs(int button,int cnt);
int main()
{
scanf("%d %d",&n,&m);
getchar();
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
char c=getchar();
a[i][j]=c-'0';
}
getchar();
}
dfs(0,0);
printf("%d\n",min);
return 0;
}
void change(int *elem)
{
if(*elem==1) *elem=0;
else *elem=1;
}
void press(int i,int j,int array[SIZE][SIZE])
{
change(&array[i][j]);
if(i>1) change(&array[i-1][j]);
if(i<n) change(&array[i+1][j]);
if(j>1) change(&array[i][j-1]);
if(j<m) change(&array[i][j+1]);
}
int calculate()
{
int res=0,b[SIZE][SIZE];/*b数组记录操作后的a数组*/
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
b[i][j]=a[i][j];
}
}
for(int i=2;i<=n;i++)/*实行灭灯操作*/
{
for(int j=1;j<=m;j++)
{
if(b[i-1][j]==1)
{
press(i,j,b);
res++;
}
}
}
for(int j=1;j<=m;j++)/*检测是否有解*/
{
if(b[n][j]==1)
{
res=-1;
break;
}
}
return res;
}
void dfs(int button,int cnt)
{
if(button==m)/*递归边界*/
{
int times=calculate();
if(times!=-1 && times+cnt<min)
{
min=times+cnt;
}
return ;
}
press(1,button+1,a);/*继续搜索*/
dfs(button+1,cnt+1);
press(1,button+1,a);
dfs(button+1,cnt); /* 回溯前一定要复原 */
return ;
}
五、个人感想
1.研究递归函数时,一定要站在宏观的角度看待每一部分的作用,不要纠结于递归具体实现时的细节;
2.dfs回溯前一定要复原:
press(1,button+1,a);
dfs(button+1,cnt+1);
press(1,button+1,a);
dfs(button+1,cnt);
否则可能乍一看没问题,却在递归实现的过程中哪里出乎我们的意料,很难改正。
比如作者一开始是这样写的:
dfs(button+1,cnt);
press(1,button+1,a);
dfs(button+1,cnt+1);
在递归的过程中,cnt的值与我预期的不一致,导致结果错误……
七、其他方法
最后,我认为灯光的明灭可以用二进制的方式来表示,看大佬的帖子,也可以用所谓异或方程高斯消元法的方法来解,以后有机会再了解一下吧qwq
八、小学期感想
到现在为止,也已经做了几道题了,给我的感受是每道题的问题乍一眼看会很复杂,但是一定要自己思考,抓住问题的特点,弄清问题的本质,如果有些问题实在不知道该怎么解决,也可到csdn上查询(这里强烈推荐@贝贝今天AC了吗,公众号鸡翅编程,真滴是保姆级教程)但一定要自己思考成熟后再去敲代码。think twice,coding once。
法二
把每一行灯的明灭情况当做一个二进制数,对每一行灯的操作也当成一个二进制数,这样按灯操作可以用二进制的按位异或来模拟(常用按位异或模拟加法,按位与模拟乘法)