AcWing 95. 费解的开关(python超详细讲解)(递推)(位运算)

本题是AcWing上的一道经典递推题,网上的讲解很多,但大都不是很详细,python的讲解更是少之又少,这里给大家详细讲解一下本题。

题目:

题目链接:95. 费解的开关

你玩过“拉灯”游戏吗?25 盏灯排成一个 5×5 的方形。每一个灯都有一个开关,游戏者可以改变它的状态。每一步,游戏者可以改变某一个灯的状态。游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。我们用数字 1 表示一盏开着的灯,用数字 0 表示关着的灯。下面这种状态

10111
01101
10111
10000
11011

在改变了最左上角的灯的状态后将变成:

01111
11101
10111
10000
11011

再改变它正中间的灯后状态将变成:

01111
11001
11001
10100
11011

给定一些游戏的初始状态,编写程序判断游戏者是否可能在 6 步以内使所有的灯都变亮。

输入格式
第一行输入正整数 n,代表数据中共有 n 个待解决的游戏初始状态。以下若干行数据分为 n
组,每组数据有 5 行,每行 5 个字符。每组数据描述了一个游戏的初始状态。各组数据间用一个空行分隔。

输出格式
一共输出 n 行数据,每行有一个小于等于 6 的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。对于某一个游戏初始状态,若 6 步以内无法使所有灯变亮,则输出 −1。

数据范围
0<n≤500

输入样例:

3

00111
01011
10001
11010
11100

11101
11101
11110
11111
11111

01111
11111
11111
11111
11111

输出样例:

3
2
-1

思路:

相信很多小伙伴看到本题后完全没有思路,不知道该怎样去做。我也和大家一样,看了很多题解并自己琢磨了半天才搞明白。

首先要理清一点,本题是递推的思想,所有灯泡的状态是由上一层灯泡决定的。什么意思呢?

我们先从第二行灯泡看起,如过第二层灯泡中有灯泡是0(灭),位置是(i,j),则需按下该灯泡的下面的灯泡开关(i + 1, j),使(i, j)位置处的灯泡发光,为什么要按下一行的灯泡来使之为1(亮)呢?因为我们必须确定一个方向(从上往下,或从左往右,或从下往上,或从右往左),从该方向开始使灯泡变亮,且不能改变已经遍历过的灯泡的状态(不然灯泡状态一直在改变,没有固定的灯泡状态也就无法求解)。

按照这样的方式,我们就可以将第二行的灯泡开始一直到第四的灯泡全部变亮,此时只需判断第五行(也就是最后一行的灯泡)是否有灭的,如果有说明此方案无解(无法使所有灯泡都变亮),因为最后一行的灯泡是不能改变的,如果改变了,之前所有的灯泡状态都会随之发生改变。

从第二行开始到第五行的灯泡状态都可以确定了,那么第一行的灯泡状态如何确定呢?为什么不能从第一行开始就使用上述方法呢?

因为本题是求最小步数,而且第一行灯泡是初始状态,可以任意改变,不需要灯泡是灭的才能按开关。第一行的状态发生了变化,之后每一行的状态都会发生变化,所需步数也会随之改变。至于怎么确保第一行的灯泡是全亮呢,方法跟第二到第四行的灯泡全亮的方法一样,只不过这里要先固定第一行才能确定之后的行数,这样之后的过程才可以继续。

因为灯泡数始终是 5 * 5,所以第一行的操作可能有32种( 2 5 2^5 25)(每个位置有两种按法,一共五个位置),把每个操作的所有可能结果都遍历一遍,寻找最小解并保存就可以求出答案。

枚举第一行的意义是:不需要在意第一行的灯是灭是暗,只需把第一行的按法枚举一遍,也就是我们说的 “操作”,每个位置都有两种选择,按(用1表示)或者不按(用0表示),遍历这32种操作引发的情况,每一次再通过res = min(res, step);把最小步数存一下,就能找到最优解。

补充为什么要枚举第一行的所有情况:
1,第一行确定余下四行的开灯结果是固定的是必然的发生的
2,第一行的五个开关也是可以按动的,不同的按动会有不同的结果共有2的5次方种结果(固定第一行)

大体思路:

// 思路:我们枚举第一行的点击方法,共32种,完成第一行的点击后,固定第一行,
// 从第一行开始递推,若达到第n行不全为0,说明这种点击方式不合法。
// 在所有合法的点击方式中取点击次数最少的就是答案。
// 对第一行的32次枚举涵盖了该问题的整个状态空间,因此该做法是正确的

这里直接放代码进行讲解:

import copy

n = int(input())
# 对某一个位置的灯进行操作后,会以十字型的方式改变周围灯的状态,通过偏移量进行改变
dirs = [[0, 1], [1, 0], [-1, 0], [0, -1], [0, 0]]  # 改变周围灯的状态的偏移量


def turn(x, y):
    # 枚举5个偏移量,new_x,new_y表示偏移量之后的位置
    for d in dirs:
        new_x = x + d[0]
        new_y = y + d[1]
        # 判断加入偏移量后是否出界了
        if new_x < 0 or new_y < 0 or new_x >= 5 or new_y >= 5:
            continue  # 在边界外,直接忽略即可
        # 如果在边界内,就改变这个位置的灯的状态(将0变成1,1变成0) 这里直接用异或(^)来表示
        path[new_x][new_y] ^= 1


for _ in range(n):
    num = []
    for i in range(5):
        num.append(list(map(int, input())))
    if _ < n - 1:  # 满足输入格式
        p = input()
    # res:最大值(最多需要多少步),超过6步就算无解,所以这里可以定义一个比较大的数
    res = 100
    # 枚举所有的操作方案
    for op in range(32):  # 第一行的操作(一共有32种用二进制表示:0~31)
        #   这里选择在备份棋盘上进行操作(因为要找到最小步数,每次新的操作方案都要对原棋盘进行操作才可以)
        path = copy.deepcopy(num)  # 下面会详细说明
        step = 0  # step:记录最小步数

        for i in range(5):
            # 判断当前这个操作方案op的二进制的这一位是否需要操作,如果是0就操作一下,步数 + 1(这一步的详细讲解在后面)
            if op >> i & 1 == 0:
                step += 1
                turn(0, i)  # 改变周围十字型的灯的状态,0:代表第一行,i:代表第一行的第几位
        '''
        当前操作方案操作完第一行之后,又决定了后面一行的操作,
        每一行都决定后面一行的操作,一直影响到倒数第二行为止
        '''
        for i in range(4):
            for j in range(5):
                '''
                依次判断每一行每个灯的状态,如果当前这个灯的状态是灭的(也就是0),
                则下一行的同列的这个位置就要按一下开关了
                '''
                if path[i][j] == 0:
                    step += 1  # 加一步
                    turn(i + 1, j)  # 操作下一行的同列的这个位置
        cnt = False
        '''
        最后一行灯的状态是不可以再进行单独操作的了,因为没有再下一行决定最后一行怎样操作了,
        这时如果最后一行灯的状态不是全亮(1)的情况就则说明当前的这个第一行的操作方案(op)是无解的
        '''
        for j in range(5):
            if path[4][j] == 0:
                cnt = True
                break
        # 如果最后一行没有黑的灯,则进行比较选出最少的步数
        if cnt == False:
            res = min(res, step)

    if res > 6:  # 如果最终最少步数还是大于6,说明无解,则答案为-1
        res = -1
    print(res)

这段代码有一个难点 op >> i & 1 == 0 和一个知识点 path = copy.deepcopy(num)

首先讲解一下 op >> i & 1 == 0 是什么意思:
其中,op保存的根本就不是第一行的灯所有可能的状态,不然它第 i 位都为1了还按它干嘛? op单纯只是保存了第一行按开关的32种方式,与输入数据无关。

op在二进制下某位为0就代表我们选择按下这一位所在编号的开关,你也可以自己规定k在二进制下某位为1才代表我们选择按下这一位所在编号的开关,这都无所谓。这里主要是固定第一行的灯泡并且固定第一行灯泡所需要的步数!!!

path = copy.deepcopy(num)是深拷贝
这个知识点也是我头一次了解,之前一直都是用 path = num[:] (副本浅拷贝),也没有出现错误,遇到这道题终于栽了,花了老长时间才发现自己错哪了。

这里我专门写了一篇博客来讲解浅拷贝和深拷贝的区别和用法python中对列表的复制操作(浅拷贝与深拷贝) 欢迎大家指点。

总结:

本题涉及的知识点还是比较广的,难点也比较多,一个是递推的确立,一个是第一行的固定,一个是如何用代码实现第一行的固定,还有一个是python标准库copy的使用(深拷贝与浅拷贝的区别与使用)。

  • 18
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不染_是非

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

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

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

打赏作者

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

抵扣说明:

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

余额充值