LC841 钥匙和房间:从 0 号房间出发,能否进入所有房间
你有 n 个房间,一开始只有 0 号房间是打开的。每个房间里可能有一些钥匙,房间 i 里的钥匙 x 表示可以打开 x 号房间。问题是:从 0 号房间出发,最后能不能进入所有房间?
你有 n 个房间,一开始只有 0 号房间是打开的。每个房间里可能有一些钥匙,房间 i 里的钥匙 x 表示可以打开 x 号房间。问题是:从 0 号房间出发,最后能不能进入所有房间?
当前节点的合法邻居是谁,访问标记何时写入?
把房间看成节点、钥匙看成有向边,从 0 出发做 DFS/BFS,用 visited 标记访问过的房间,避免重复访问和环。
先说结论:这道题到底解决什么
怎样从“你有 n 个房间,一开始只有 0 号房间是打开的。每个房间里可能有一些钥匙,房间 i 里的钥匙 x 表示可以打开 x 号房间。问题是:从 0 号房间出发,最后能不能进入所有房间?”推导出 图 DFS/BFS · 有向图可达性,并证明每次状态变化都不会漏掉答案?
中心结论:把房间看成节点、钥匙看成有向边,从 0 出发做 DFS/BFS,用 visited 标记访问过的房间,避免重复访问和环。
- 1.暴力方案在哪里重复计算,为什么仍然是正确基线?
- 2.当前节点的合法邻居是谁,访问标记何时写入?
- 3.不变量“从 0 号节点出发的有向图可达性问题,不要求回到 0。”为什么能保证算法安全前进?
完整题目与题意拆解
有 N 个房间,开始时你位于 0 号房间。每个房间有不同的号码:0,1,2,...,N-1,并且房间里可能有一些钥匙能使你进入下一个房间。
在形式上,对于每个房间 i 都有一个钥匙列表 rooms[i],每个钥匙 rooms[i][j] 由 [0,1,...,N-1] 中的一个整数表示,其中 N = rooms.length。 钥匙 rooms[i][j] = v 可以打开编号为 v 的房间。最初,除 0 号房间外的其余所有房间都被锁住。你可以自由地在房间之间来回走动。如果能进入每个房间返回 true,否则返回 false。
1 <= rooms.length <= 1000 0 <= rooms[i].length <= 1000 所有房间中的钥匙数量总计不超过 3000。
在本站主例中,你有 n 个房间,一开始只有 0 号房间是打开的。每个房间里可能有一些钥匙,房间 i 里的钥匙 x 表示可以打开 x 号房间。问题是:从 0 号房间出发,最后能不能进入所有房间?
算法最终需要得到或观察:遍历结束后 visited 全 true 返回 true,否则 false。
- • 输入:你有 n 个房间,一开始只有 0 号房间是打开的。每个房间里可能有一些钥匙,房间 i 里的钥匙 x 表示可以打开 x 号房间。问题是:从 0 号房间出发,最后能不能进入所有房间?
- • 机器需要维护:stack/queue 存待处理房间,visited 数组记录哪些房间已打开。
- • 最终可观察结果:遍历结束后 visited 全 true 返回 true,否则 false。
先看清算法到底要维护什么
先建立输入、目标、输出和第一批状态,不急着进入模板。
rooms=[[1,3],[3,0,1],[2],[0]],0 号房间默认打开
第一层方案:暴力做法
重复从多个位置搜索会反复访问同一状态;统一的 visited 与前线结构让每个状态只处理一次。
暴力方案的价值是确认题意并提供正确性基线。它通常会覆盖所有候选, 但没有保存已经确认的信息,因此同一状态会被重新计算。
重复工作究竟发生在哪里
把重复读取或重复搜索的区域明确标出,再决定优化必须保存什么。
rooms=[[1,3],[3,0,1],[2],[0]],0 号房间默认打开
整体地图:先做什么,再做什么
- 1建模把输入翻译成“图搜索前线”,明确答案需要观察什么。
- 2状态只维护 stack/queue 存待处理房间,visited 数组记录哪些房间已打开。
- 3转移每一步按照 把房间看成节点、钥匙看成有向边,从 0 出发做 DFS/BFS,用 visited 标记访问过的房间,避免重复访问和环。
- 4收尾读取 遍历结束后 visited 全 true 返回 true,否则 false。,并复核边界与复杂度。
图搜索前线:核心概念
先把题目对象翻译成节点和边,再选择 BFS 或 DFS。
这道题不是为了记住一个组件名称,而是为了让状态具有可解释的语义:stack/queue 存待处理房间,visited 数组记录哪些房间已打开。
- • 从 0 号节点出发的有向图可达性问题,不要求回到 0。
- • visited 防止重复访问同一个房间,也避免房间互相有钥匙时死循环。
建立“图搜索前线”心智模型
用主例建立核心状态,先预测下一步,再公开正确分支和理由。
rooms=[[1,3],[3,0,1],[2],[0]],0 号房间默认打开
核心机制:状态如何一步步变化
给出一个房间数组,每个房间里面装了一些钥匙。0 号房间默认是可以进入的,房间进入顺序没有要求,问最终能否进入所有房间。 用 DFS 依次深搜所有房间的钥匙,如果都能访问到,最终输出 true。这题算是 DFS 里面的简单题。
把房间看成节点、钥匙看成有向边,从 0 出发做 DFS/BFS,用 visited 标记访问过的房间,避免重复访问和环。
执行过程中持续维护:stack/queue 存待处理房间,visited 数组记录哪些房间已打开。
正确性依赖以下不变量:从 0 号节点出发的有向图可达性问题,不要求回到 0。;visited 防止重复访问同一个房间,也避免房间互相有钥匙时死循环。
面试时可以压缩为:建模成有向图可达性:房间是节点、钥匙是有向边,从 0 做 DFS/BFS,用 visited 避免重复访问和环,最后看 visited 是否全 true。时间 O(n+m),空间 O(n)。
落到当前题,执行机制可以压缩为:把房间看成节点、钥匙看成有向边,从 0 出发做 DFS/BFS,用 visited 标记访问过的房间,避免重复访问和环。每一次更新都必须保持核心不变量,而不是只让样例碰巧得到正确答案。
一次状态转移为什么成立
集中观察一次选择、计算和状态写回,让变量变化与原因同时出现。
Room 0 打开了 R1、R3
正确性证明:为什么不会漏答案
初始化:算法开始时,全部合法候选仍在状态表示范围内;从 0 号节点出发的有向图可达性问题,不要求回到 0。
保持:执行“把房间看成节点、钥匙看成有向边,从 0 出发做 DFS/BFS,用 visited 标记访问过的房间,避免重复访问和环。”时,只删除已经能证明不可能的候选,并把新信息写回 stack/queue 存待处理房间,visited 数组记录哪些房间已打开。
终止:没有待处理状态或达到命中条件时,当前可观察结果就是“遍历结束后 visited 全 true 返回 true,否则 false。”。
完整执行过程
- 1题目与输入你有 n 个房间,一开始只有 0 号房间是打开的。每个房间里可能有一些钥匙,房间 i 里的钥匙 x 表示可以打开 x 号房间。问题是:从 0 号房间出发,最后能不能进入所有房间? 因为:把房间看成节点、钥匙看成有向边,从 0 出发做 DFS/BFS,用 visited 标记访问过的房间,避免重复访问和环。
- 2从 0 号房间出发做 DFS0 号房间一开始就是打开的,入栈。 因为:钥匙是有向边,从 0 出发遍历所有可达房间。
- 3访问 Room 0,钥匙 [1,3]Room 0 的钥匙打开了 Room 1、3,入栈等待处理。 因为:visited 防止重复访问同一个房间,也避免房间互相有钥匙时死循环。
- 4访问 Room 3,钥匙 [0]Room 3 的钥匙对应房间都访问过,visited 把它们挡住,跳过。 因为:visited 防止重复访问同一个房间,也避免房间互相有钥匙时死循环。
- 5预测下一步先不要看下一帧——根据当前不变量,预测算法接下来会怎么动。 因为:主动预测会暴露你对不变量的真实理解,比被动看动画有效得多。
- 6访问 Room 1,钥匙 [3,0,1]Room 1 的钥匙对应房间都访问过,visited 把它们挡住,跳过。 因为:visited 防止重复访问同一个房间,也避免房间互相有钥匙时死循环。
- 7Room 2 永远到不了遍历结束后扫描 visited,判断能否进入所有房间。 因为:有向图可达性:从 0 能走到每个节点则返回 true。
- 8收尾与复杂度遍历结束后 visited 全 true 返回 true,否则 false。 因为:时间 O(n + m) · 空间 O(n)。把房间看成节点、钥匙看成有向边,从 0 出发做 DFS/BFS,用 visited 标记访问过的房间,避免重复访问和环。
从输入完整走到可观察结果
从主例第一帧运行到答案,时间轴始终显示当前状态、因果解释和下一步。
rooms=[[1,3],[3,0,1],[2],[0]],0 号房间默认打开
把动画和 Go 代码逐行对应
代码窗口不会按容易漂移的固定数字行号硬绑动画,而是根据当前语义阶段, 在完整 Go 实现中定位选择、计算、写回或返回分支。当前变量与高亮行一起变化。
让每个动作都落到 Go 分支
重新执行关键分支,只显示当前代码附近窗口,并说明这行为什么在此刻运行。
rooms=[[1,3],[3,0,1],[2],[0]],0 号房间默认打开
完整 Go 提交代码与最小测试
func canVisitAllRooms(rooms [][]int) bool {
visited := make(map[int]bool)
visited[0] = true
dfsVisitAllRooms(rooms, visited, 0)
return len(rooms) == len(visited)
}
func dfsVisitAllRooms(es [][]int, visited map[int]bool, from int) {
for _, to := range es[from] {
if visited[to] {
continue
}
visited[to] = true
dfsVisitAllRooms(es, visited, to)
}
}func main() {
// 1. 主例
// 输入:mode="keys-and-rooms", rooms=[[1,3],[3,0,1],[2],[0]]
// 期望:遍历结束后 visited 全 true 返回 true,否则 false。
//
// 2. 失败 / 未命中
// 检查:钥匙在房间内,rooms[i] 的每个值都是一条 i → key 的有向边。
//
// 3. 边界
// 空输入、单元素、最小合法规模,以及答案恰好落在边界的情况。
//
// 4. 迁移
// LC207 课程表 · 有向图拓扑;LC547 省份数量 · 无向图连通块;LC1971 寻找图中是否存在路径
}Go 参考实现基于 halfrost/LeetCode-Go 的 MIT 许可代码整理,并按本站教学结构补充解释与动画映射。
正确性与复杂度
执行过程中只保留仍可能影响答案的状态。把房间看成节点、钥匙看成有向边,从 0 出发做 DFS/BFS,用 visited 标记访问过的房间,避免重复访问和环。
额外状态主要用于维护:stack/queue 存待处理房间,visited 数组记录哪些房间已打开。
- • 从 0 号节点出发的有向图可达性问题,不要求回到 0。
- • visited 防止重复访问同一个房间,也避免房间互相有钥匙时死循环。
最容易写错的地方
钥匙在房间内,rooms[i] 的每个值都是一条 i → key 的有向边。
空间不是 O(1):visited 数组需要 O(n)。
必须额外检查空输入、单元素、未命中或不可达情况,以及恰好落在边界的输入。
最后复盘:带走逻辑链
- 1题意你有 n 个房间,一开始只有 0 号房间是打开的。每个房间里可能有一些钥匙,房间 i 里的钥匙 x 表示可以打开 x 号房间。问题是:从 0 号房间出发,最后能不能进入所有房间?
- 2重复重复从多个位置搜索会反复访问同一状态;统一的 visited 与前线结构让每个状态只处理一次。
- 3优化把房间看成节点、钥匙看成有向边,从 0 出发做 DFS/BFS,用 visited 标记访问过的房间,避免重复访问和环。
- 4证明从 0 号节点出发的有向图可达性问题,不要求回到 0。;visited 防止重复访问同一个房间,也避免房间互相有钥匙时死循环。
- 5复杂度时间 O(n + m),空间 O(n)
- • LC207 课程表 · 有向图拓扑
- • LC547 省份数量 · 无向图连通块
- • LC1971 寻找图中是否存在路径