在问 · 算法题解

752. 打开转盘锁

中等图论广度优先搜索力扣原题 ↗

题目描述

一把密码锁有 4 个转轮,每个转轮上是数字 0 到 9。一次操作只能把某一个转轮向上或向下拨动一格,并且首尾相接:9 再拨一格变成 0,0 反向拨一格变成 9。

锁的当前状态用 4 位数字字符串表示,初始为 "0000"。字符串数组 deadends 列出若干禁止状态,锁一旦处于其中任意一个状态就会卡死,不能再转动。

输入 deadends 和目标状态 target。返回从 "0000" 变到 target 所需的最少操作次数。如果无法到达,返回 -1。

示例 1
输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6

解释:一条可行路线是 0000→1000→1100→1200→1201→1202→0202,共 6 步;更短的路线会经过禁止状态。

示例 2
输入:deadends = ["8888"], target = "0009"
输出:1

解释:把最后一位从 0 反向拨一格得到 9。

示例 3
输入:deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"]
target = "8888"
输出:-1

解释:8888 的 8 个相邻状态全是禁止状态,无法到达。

数据范围

  • 1 <= deadends.length <= 500
  • deadends[i].length == 4
  • target.length == 4
  • target 不会出现在 deadends 中。
  • target 和 deadends[i] 都只由数字字符组成。

解题思路:广度优先搜索

把每个 4 位状态看成图中的一个节点,每个节点有 8 个邻居(4 个转轮 × 上下两个方向)。从 "0000" 出发做按层 BFS:第 k 层就是恰好需要 k 次旋转才能到达的状态。跳过死亡数字和已访问的状态;第一次在某层遇到 target 时,层号就是答案。

为什么正确

处理第 steps 层时,frontier 恰好包含所有最少需要 steps 次旋转、且路径上不经过死亡数字的状态。

复杂度分析

时间 O(10^4 × 8 × 4):最多 10^4 个状态,每个生成 8 个邻居、每个邻居拼接长度 4 的字符串。空间 O(10^4)。

易错点

"0000" 本身就是死亡数字时要直接返回 -1。另外要在加入队列时就标记 seen,否则同一状态会被多次入队,状态数会爆炸。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        dead = set(deadends)
        if '0000' in dead:
            return -1
        seen = {'0000'}
        frontier = ['0000']
        steps = 0
        while frontier:
            nxt = []
            for cur in frontier:
                if cur == target:
                    return steps
                for i in range(4):
                    d = int(cur[i])
                    for nd in ((d + 1) % 10, (d + 9) % 10):
                        s = cur[:i] + str(nd) + cur[i + 1:]
                        if s not in seen and s not in dead:
                            seen.add(s)
                            nxt.append(s)
            frontier = nxt
            steps += 1
        return -1

载入编辑器,自己跑一遍 →

其他解法

单向广度优先是标准解法,代码简单;双向广度优先在状态空间大、起点和终点都已知时能大幅减少搜索量。

解法 2:双向广度优先搜索 时间 O(10⁴ · 8 · 4),空间 O(10⁴)

同时从 '0000' 和 target 两端搜索,各自维护一个当前层集合。每次选规模较小的一端扩展一层,生成的邻居如果在另一端的集合里,说明两边相遇,答案是两端已走步数之和。两端共用一个 visited 和 deadends 集合来剪枝。

最坏复杂度不变,但搜索树从一棵深 d 的变成两棵深 d/2 的,实际访问的状态通常少很多。代价是要维护两个集合并处理相遇判断,写法更复杂。

class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        dead = set(deadends)
        if '0000' in dead or target in dead:
            return -1
        if target == '0000':
            return 0
        front, back = {'0000'}, {target}
        seen = {'0000', target}
        steps = 0
        while front and back:
            if len(front) > len(back):
                front, back = back, front
            steps += 1
            nxt = set()
            for cur in front:
                for i in range(4):
                    d = int(cur[i])
                    for nd in ((d + 1) % 10, (d + 9) % 10):
                        s = cur[:i] + str(nd) + cur[i + 1:]
                        if s in back:
                            return steps
                        if s not in seen and s not in dead:
                            seen.add(s)
                            nxt.add(s)
            front = nxt
        return -1

图论 专题的其他题目

查看 图论 全部题目 →