打开转盘锁:密码当点,拨一格当边
四位数字,每位能往前拨或往后拨一格,0 和 9 连成环。从 0000 一层层拨出去,死亡密码当作墙,先碰到目标的层数就是最少步。
每个密码有 8 个邻居(四位各 ±1,0 与 9 环回)。从 0000 做层序 BFS,死亡数字和已访问的点跳过。第一次到达 target 的层数即最少拨动次数;起点就是死亡数字或走尽仍未到达则返回 -1。时间与空间都按 10⁴ 个状态计。
锁上有四个转轮,每位是 0 到 9,可以朝两个方向拨一格,9 再往前变成 0,0 再往后变成 9。每次只拨一位。给你目标密码,以及若干死亡数字:指针走到这些密码上锁就卡死,这条路作废。求从 0000 拨到目标的最少次数;没法到达返回 -1。
主例 target = 0202,死亡数字含 0201、0101、0102、1212、2002。一条合法路线是 0000→1000→1100→1200→1201→1202→0202,一共 6 步。看起来更短的 0000→0001→0002→0102→0202 会在 0102 卡死。
密码只有一万种,每次 8 个邻居,这是一张有限的无权图。本文要回答:为什么最短拨动次数等于 BFS 层数,第一步那 8 个邻居是谁,以及死亡数字怎样把近路堵死。
8 个邻居按层扩散
第一直觉是对着目标一位一位拨:要把 0000 变成 0202,第二位拨两次似乎就够。可是路上可能撞上死亡数字,而且有时得先往远处绕,再转回来。深度优先搜索能找到某条合法路,但不保证最短。每位独立贪心,也处理不了「中间那位不能经过 0102」。
缺的是一张把状态当点的图。一个四位字符串就是一个点。从任意点出发,选一位加一或减一,得到 8 个邻居,这就是 8 条边。0 减一变成 9,9 加一变成 0,边在数字上是环形的。所有拨动代价都是 1,最短拨动次数就是无权图最短路,用队列一层一层扩。
死亡数字相当于墙上的点:生成邻居时看见它就当没这条边。已经进来过的密码也要记下,否则在一万个点上来回拨会转圈。起点 0000 若本身是死亡数字,一步都迈不出去,直接 -1。目标就是 0000,一步不用拨,返回 0。
用手走主例。步 0 只有 0000,演示第一帧。拨一步,四位各加一、各减一,得到 0001、0010、0100、1000、9000、0900、0090、0009;这一层 8 个都不是死亡数字,演示第二帧全部列出。
之后继续按层扩。想走 0001→0002→0102 的人会在 0102 被挡,0201、2002 也会挡住另一些近路。队列第一次把 0202 当作邻居拿出来时,层数是 6。一条没撞墙的路是 0000、1000、1100、1200、1201、1202、0202。
密码空间封顶一万,每点最多扩 8 次,搜索一定结束。队列空了还没见过目标,返回 -1。双向 BFS 从起点和目标同时扩,能减少相遇之前的层数,但教学上单向层序已经能把「步数 = 层数」说清楚。不要用访问数组之前先拨再判断死亡,否则可能把死亡数字入队,后续又从墙上往外走。
状态图点是密码,边是拨一格。边权全是 1,所以 BFS 的层数就是最少步。死亡数字不是「到了再回头」,而是根本不进队列。主例 6 步是绕开 0102、0201 之后的最短路。
Go:状态图 BFS
func openLock(deadends []string, target string) int {dead := map[string]bool{}for _, d := range deadends { dead[d] = true }if dead["0000"] { return -1 }if target == "0000" { return 0 }q := []string{"0000"}visited := map[string]bool{"0000": true}steps := 0for len(q) > 0 {steps++size := len(q)for i := 0; i < size; i++ {cur := q[0]; q = q[1:]for _, nb := range neighbors(cur) {if nb == target { return steps }if dead[nb] || visited[nb] { continue }visited[nb] = trueq = append(q, nb)}}}return -1}
1先把死亡数字放进集合。起点就在集合里,直接 -1;目标就是起点,直接 0。
2外层 steps++ 再按 size 切层。主例第一层扩出那 8 个邻居,steps 变成 1;第一次把 0202 当邻居看见时 steps 是 6。
3邻居是死亡或访问过就跳过,不要入队。从墙上出发会数出一条不合法的短路径。
总结
密码当点、拨一格当边,BFS 层数即最少步。主例绕开死亡数字要 6 步。
- 每位 ±1 共 8 个邻居,0 和 9 环回。主例第一步就是那 8 个串。
- 0102、0201 这类死亡数字不入队。贪心少拨几位会撞墙。
- 起点是死亡数字返回 -1。搜完没有目标也返回 -1。