打开转盘锁

标签: 广度优先搜索 数组 哈希表 字符串

难度: Medium

一个密码锁由 4 个环形拨轮组成,每个拨轮都有 10 个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0''0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。

锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。

列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。

字符串 target 代表可以解锁的数字,请给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1

示例 1:

输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,因为当拨动到 "0102" 时这个锁就会被锁定。

示例 2:

输入: deadends = ["8888"], target = "0009"
输出:1
解释:
把最后一位反向旋转一次即可 "0000" -> "0009"。

示例 3:

输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
输出:-1
解释:
无法旋转到目标数字且不被锁定。

示例 4:

输入: deadends = ["0000"], target = "8888"
输出:-1

提示:

  • 1 <= deadends.length <= 500
  • deadends[i].length == 4
  • target.length == 4
  • target 不在 deadends 之中
  • targetdeadends[i] 仅由若干位数字组成

注意:本题与主站 752 题相同: https://leetcode-cn.com/problems/open-the-lock/

Submission

运行时间: 91 ms

内存: 16.7 MB

from collections import deque

class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        deads = set(deadends)
        q1, q2 = set(), set()
        q1.add('0000')
        q2.add(target)
        step = 0

        while q1 and q2:
            if len(q1) > len(q2):
                q1, q2 = q2, q1
            temp = set()
            for cur in q1:
                if cur in deads:
                    continue
                if cur in q2:
                    return step
                deads.add(cur)
                for j in range(4):
                    up = self.plusOne(cur, j)
                    if up not in deads:
                        temp.add(up)
                    down = self.minusOne(cur, j)
                    if down not in deads:
                        temp.add(down)
            step += 1
            q1, q2 = q2, temp
        return -1

    def plusOne(self, s: str, j: int) -> str:
        cha = list(s)
        if cha[j] == '9':
            cha[j] = '0'
        else:
            cha[j] = chr(ord(cha[j])+1)
        return ''.join(cha)
    def minusOne(self, s: str, j: int) -> str:
        cha = list(s)
        if cha[j] == '0':
            cha[j] = '9'
        else:
            cha[j] = chr(ord(cha[j])-1)
        return ''.join(cha)

Explain

本题解采用了双向广度优先搜索(BFS)算法。首先将起始点'0000'和目标点target分别加入到两个集合q1和q2中。算法交替从q1和q2中的一个较小的集合扩展,以此达到双向搜索的目的。每一步都检查当前节点是否出现在对方集合中,如果出现,表示找到了最短路径。如果没有,就将当前节点的所有相邻节点(通过拨动锁一位得到)添加到临时集合中,然后将q1和q2交换,继续下一轮搜索。搜索过程中会跳过出现在deadends集合中的节点。

时间复杂度: O(N)

空间复杂度: O(N)

from collections import deque

class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        deads = set(deadends)  # 将deadends转换为集合以便快速查找
        q1, q2 = set(), set()  # 使用集合进行双向BFS
        q1.add('0000')  # 起始节点
        q2.add(target)  # 目标节点
        step = 0  # 初始步数为0

        while q1 and q2:  # 当两个队列非空时继续搜索
            if len(q1) > len(q2):  # 始终扩展较小的集合以提高效率
                q1, q2 = q2, q1
            temp = set()  # 临时集合存储新扩展的节点
            for cur in q1:
                if cur in deads:  # 如果当前节点是死亡节点,则跳过
                    continue
                if cur in q2:  # 如果当前节点已经在另一个集合中,说明找到了最短路径
                    return step
                deads.add(cur)  # 标记当前节点已访问
                for j in range(4):  # 遍历四个轮盘
                    up = self.plusOne(cur, j)  # 向上拨动一位
                    if up not in deads:
                        temp.add(up)
                    down = self.minusOne(cur, j)  # 向下拨动一位
                    if down not in deads:
                        temp.add(down)
            step += 1  # 增加步数
            q1, q2 = q2, temp  # 交换搜索方向
        return -1  # 如果两个队列都为空,则无法到达目标

    def plusOne(self, s: str, j: int) -> str:
        cha = list(s)
        if cha[j] == '9':
            cha[j] = '0'
        else:
            cha[j] = chr(ord(cha[j])+1)
        return ''.join(cha)
    def minusOne(self, s: str, j: int) -> str:
        cha = list(s)
        if cha[j] == '0':
            cha[j] = '9'
        else:
            cha[j] = chr(ord(cha[j])-1)
        return ''.join(cha)

Explore

在双向BFS中,选择扩展较小的集合可以显著提高搜索效率,因为这样做可以减少每一步可能的搜索空间和计算量。具体来说,如果有两个集合A和B,A的大小小于B,那么从A出发的可能的新状态数量也相对较少。这种策略的数学基础在于减少了搜索的广度,因为在每一步中扩展的是节点数量较少的集合,理论上能减少重复计算和不必要的搜索,从而更快地找到交点,实现效率上的优化。这种方法在某种程度上类似于优化搜索树的扩展过程,减少了不必要的分支。

是的,算法有处理这种情况的机制。在算法实现中,将deadends转换为集合deads,然后在每次遍历当前节点前都会检查该节点是否在deads中。如果起始节点'0000'在deads中,它会在第一次检查时被识别并跳过,此时由于没有其他节点可以扩展,搜索将结束并返回-1,表示无法到达目标。这样确保了算法在初始状态即为死锁时能正确地返回无解的结果。

在双向BFS实现中,每次扩展节点时,都会检查当前节点是否已存在于对方的集合中。算法通过在每个扩展步骤中交替检查当前正在扩展的节点集合中的每个节点是否已经出现在对方集合中来确保有效检测到路径的交点。如果发现某个节点同时存在于两个集合中,这意味着从起点到该节点和从终点到该节点的路径在此节点处相遇,因此找到了从起点到终点的有效路径。此时算法将返回当前累计的步数作为最短路径的长度。这种交叉检查机制确保了即使在两个队列都非空的情况下,也能及时发现并处理路径的交点。