【BIT2021程设】2. 解谜游戏——初见DFS

写在前面:

本系列博客仅作为本人十一假期过于无聊的产物,对小学期的程序设计作业进行一个总结式的回顾,如果将来有BIT的学弟学妹们在百度搜思路时翻到了这一条博客,也希望它能对你产生一点帮助(当然,依经验来看,每年的题目也会有些许的不同,所以不能保证每一题都覆盖到,还请见谅)。

不过本人由于学艺不精,代码定有许多不足之处,欢迎各位一同来探讨。

同时请未来浏览这条博客的学弟学妹们注意,对于我给出完整代码的这些题,仅作帮助大家理解思路所用(当然,因为懒,所以大部分题我都只给一个伪代码)。Anyway,请勿直接复制黏贴代码,小学期的作业也是要查重的,一旦被查到代码重复会严厉扣分,最好的方法是浏览一遍代码并且掌握相关的要领后自己手打一遍,同时也要做好总结和回顾的工作,这样才能高效地提升自己的代码水平。

加油!


成绩10开启时间2021年08月24日 星期二 09:00
折扣0.8折扣时间2021年08月27日 星期五 23:00
允许迟交关闭时间2021年10月10日 星期日 23:00

Description

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

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

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

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

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

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

Input

第一行两个整数n,m (1\leq n,m\leq 16)

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

Output

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

Notes

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

测试用例保证一定有解


题意分析:

        看到这题我觉得多数人的第一想法是尝试从结果反推,看看这个问题是不是P问题(名词解释:NP问题、P问题),然后撞了无数次南墙(埋一个伏笔)。但实际上看到这个数据规模我们大抵也能猜到这道题的解法应该相对比较暴力,所以在粗略地分析这道问题很难有P解法之后,应该转向考虑暴力解法。

        考虑暴力解法之前,先研究一下暴力的入口在哪。首先不难发现,每个灯的有效操作就是“按一次”或者“不按”,如果按两次以上,那和前两种操作没有区别,我们可以不进行考虑;其次,最终的结果与按的顺序无关,所以我们可以从前至后地来进行遍历,暴力求解。

        然而暴力解法的问题在于,如果我们逐个地进行遍历,每个灯有两种状态,至多有16\times 16 = 256个灯,于是最多需要遍历2^{256}\approx 10^{77}次,可能多数学生对这样的数字没有概念,但做多了题目就会知道这样的做法显然是我们不能接受的,因此我们就需要考虑进行剪枝操作(名词解释:剪枝)。

        我们先从最简单的一行的情况开始看,那么总共需要遍历的次数就是2^{16}=65536,这个数字看着就和蔼可亲多了。我们可以发现,在第一行遍历完之后,第一行的某个灯的亮灭情况只能由位于其正下方的第二行的那个灯来控制了(因为只有他的上下左右和自己可以改变他的亮灭情况),那么为了达到灭掉所有灯的最终结果,我们对第二行的操作是唯一的,即对每一个第一行亮着的灯,按下位于其正下方的第二行的灯,其他灯保持不变,如果不这么操作,就无法熄灭第一行所有灯,这种情况就必然失败。我们还可以发现,在这样之后,第二行的灯的亮灭情况也仅能由第三行控制了……如此循环往复,直到按下最后一行的灯以熄灭倒数第二行的灯,此时最后一行的灯已经不能被任何灯控制了(因为他们的下方已经没有灯了),此时最后一行如果还有亮着的灯,说明此路不通,那么一定是第一行遍历到的这种情况是不可行的。

        那么现在我们就已经把所有的步骤简化为了:遍历第一行的所有情况根据遍历出的情况确定2~n行每一行的操作所有操作结束后检测第n行是否全灭

        所以遍历要如何进行呢?在这提供两种思路:

        先讲第零种,并不合理的思路,即进行至多十六层嵌套循环,每层循环两个值。这个做法的不合理之处,一是代码量太大,属于面向肝和面向脑容量编程,二是DEBUG和维护起来太麻烦,三是灵活性不够,万一哪天老板需要加到30层,你的代码就需要大改,显然不合理,在写代码的过程中要避免写出这样的代码来。

        然后讲真正意义上的第一种思路,即DFS(名词解释:DFS),在这里揭晓之前埋下的伏笔,“撞南墙”,实际上DFS就是一个不撞南墙不回头的过程,我们拿迷宫举例,DFS的算法就应该是:
        1. 自己处在某一个路口X,首先观察周围是否有可行的路(即不是墙且没有走过的路,可以想想为什么不能走“走过的路”),如果没有,直接返回失败,如果有路直接通向终点,直接返回成功,否则进入下一步;
        2. 用一个集合S记录下所有自己发现的与路口X相连的路X_i
        3. 先走向第一条,进入一个新的路口,把新的路口称为X_1,对X_1重新执行第一步。
        4. 如果执行到了这一句,只能说明X_1没能走到终点,那么就先从X_1返回X
        5. 对X_2乃至每一个X_i进行第三步。
        6. 如果执行到了这一步还是没能找到出口,说明迷宫不存在可行出口,返回失败。

        当然,这个例子也仅仅是帮助大家理解DFS这个“不撞南墙不回头”的思路,即如果还有可行的路,我就一直走,直到撞墙了,再回头,找另一可行的条路。

        当然,考虑到大家第一次写DFS,对于这一题的DFS解法我一会会贴出代码来供大家参考。

        再讲第二种思路,比较巧妙,利用二进制数的特殊性质来求解,即状态压缩(名词解释:状态压缩)。我们知道每个灯只有亮灭两种情况,实际上就对应着二进制的“0”和“1”,那么我们有没有可能用一个长度不超过16的二进制数来表示所有情况呢?答案是肯定的。这样做的好处在于我们不需要嵌套地进行遍历了,只需要从0一路线性地遍历到2^n-1,就可以遍历所有的情况。

        然而这种思路的难点在于,我确实是遍历到了每一种情况,但我怎么把这个信息从二进制数里取出来呢?我们就要用到上一题里面学到的知识——位运算。对于每个遍历到的数k,我们将其与2^i-1进行按位与“&”操作,如果结果为0,说明k的第i个灯是灭的,不为0则说明是亮的。如此我们便可以将所有的信息都提取出来。对于按灯操作,我们也只需要研究这个操作具体影响了哪一位或哪几位,将其与2^i-1进行按位异或“^”操作即可得到操作后的情况。对于检测最终的结果也非常的方便,我们只需要看其是否为0,如果不为0,说明没有全灭。总的来说,这样的做法比较巧妙,算是进阶解法,有兴趣的同学自己研究,我就不贴代码了。


    #include <bits/stdc++.h>  
    #define MAX 16  
    using namespace std;  
    int m,n;  
    int amap[MAX][MAX], copyMap[MAX][MAX];  
    //amap用于保存原灯阵和一次遍历后的灯阵, copyMap用于保存遍历后的灯阵并进行操作,两者要隔离防止互相干扰
    int ans = 256; //利用全局变量来更新最终答案,初始化为256是因为理论最大值为256  
    
    //定义一个操作,改变某处灯的情况
    void change(int amap[MAX][MAX], int row, int column){  
        if (amap[row][column] == 1) amap[row][column] = 0;  
        else if (amap[row][column] == 0) amap[row][column] = 1;  
        return;  
    }  
    //定义一个操作,模拟按下某个灯的情况
    void push(int amap[MAX][MAX], int row, int column){  
        change(amap, row, column);  
        if (row - 1 >= 0) change(amap, row - 1, column);  
        if (row + 1 < n) change(amap, row + 1, column);  
        if (column - 1 >= 0) change(amap, row, column - 1);  
        if (column + 1 < m) change(amap, row, column + 1);  
        return;  
    }  
    //定义一个操作,用于复制整个灯阵,可以防止后续的操作破坏原有的结构
    void getCopy(){  
        for(int i = 0; i < n; i++){  
            for(int j =0; j < m; j++){  
                copyMap[i][j] = amap[i][j];  
            }  
        }  
        return;  
    }  
    //定义一个函数,用于获取总共的步骤
    int getStep(){  
        getCopy();  
        int step = 0;  
        for(int i = 0; i < n - 1; i++){  
            for(int j = 0; j < m; j++){  
                if (copyMap[i][j] == 1){  
                    push(copyMap, i + 1, j);  
                    step ++;  
                }  
            }  
        }  
        for(int j = 0; j < m; j++){  
            if(copyMap[n - 1][j] == 1) return -1;  
        }  
        return step;  
    }  
    //DFS主体
    void dfs(int index, int count){  
        if(index == m){  
            int lef = getStep();  
            if(lef == -1) return;  
            else if(lef + count < ans) ans = lef + count;  
            return;  
        }  
      
        push(amap, 0, index);  
        dfs(index + 1, count + 1);  
      
        push(amap, 0, index);  //此处很重要,DFS在“回头”的时候一定要“恢复”原状态
        dfs(index + 1, count);  
      
    }  
      
    int main(){  
        scanf("%d %d",&n,&m);  
        for(int i = 0; i < n; i++){  
            for(int j = 0; j < m; j++){  
                scanf("%1d",&amap[i][j]);  
            }  
        }  
        dfs(0,0);  
        printf("%d\n",ans);  
        return 0;  
    }  

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千里之码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值