BIT计科小学期——解谜游戏(DFS解法)

Description

小张是一个密室逃脱爱好者,在密室逃脱的游戏中,你需要解开一系列谜题最终拿到出门的密码。现在小张需要打开一个藏有线索的箱子,但箱子上有下图所示的密码锁。

每个点是一个按钮,每个按钮里面有一个小灯。如上图,红色代表灯亮,白色代表灯灭。每当按下按钮,此按钮的灯以及其上下左右四个方向按钮的灯状态会改变(如果原来灯亮则灯灭,如果原来灯灭则灯亮)。如果小张通过按按钮将灯全部熄灭则能可以打开箱子。

对于这个密码锁,我们可以先按下左上角的按钮,密码锁状态变为下图。

再按下右下角的按钮,密码锁状态变为下图。

最后按下中间的按钮,灯全部熄灭。

现在小张给你一些密码锁的状态,请你告诉他最少按几次按钮能够把灯全部熄灭。

Input

第一行两个整数n,m(1<=n,m<=16)

接下来n行,每行一个长度为的01字符串,0表示灯初始状态灭,1表示灯初始状态亮。第一行两个整数。

Output

一行一个整数,表示最少按几次按钮可以把灯全部熄灭。

Notes

第一个样例见题目描述,第二个样例按左上和右下两个按钮。

测试用例保证一定有解。

测试输入期待的输出时间限制内存限制额外进程
测试用例1
  1. 3 3↵
  2. 100↵
  3. 010↵
  4. 001↵
  1. 3↵
1秒64M0
测试用例二
  1. 2 3↵
  2. 111↵
  3. 111↵
  1. 2↵
1秒64M0

一、第一印象

       刚看到这题时,我是毫无思路,不知道根据什么策略按按钮,想暴力枚举,不过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。

法二

        把每一行灯的明灭情况当做一个二进制数,对每一行灯的操作也当成一个二进制数,这样按灯操作可以用二进制的按位异或来模拟(常用按位异或模拟加法,按位与模拟乘法)

        

 法三

        异或方程高斯消元Lights Out Puzzle -- from Wolfram MathWorld

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值