钥匙和房间:从 0 号房沿钥匙走到每一间
房间是点,钥匙是有向边。初始只开了 0。从 0 做 DFS 或 BFS,能走进的房间数等于 n 才 true。
visited 只在第一次进入时标记。从 0 出发,每进一间房就把其中未访问的钥匙推进栈或队列。结束时 visited 数量等于 n 则 true,否则有房间永远拿不到钥匙。时间 O(V+E),空间 O(V+E)。
这是 LeetCode 841. Keys and Rooms。有 n 个房间,编号 0 到 n−1。rooms[i] 是房间 i 里的钥匙列表,钥匙 j 能打开房间 j。除了 0 号房一开始没锁,其余都锁着。问:能不能走进每一间。
主例 rooms = [[1],[2],[3],[]]。房间 0 里只有钥匙 1,房间 1 里只有钥匙 2,房间 2 里只有钥匙 3,房间 3 是空的。从 0 出发刚好串成一条链,四间都能进,答案 true。
第一直觉是「把所有房间扫一遍」。可你没有房间 2 的钥匙时,站在门外扫到它也进不去。缺的是可达性,不是房间名单。下面从这块缺口推出:把钥匙看成有向边,只从 0 出发走图。
钥匙是边,从 0 铺开
输入给的是每间房里的钥匙,不是一张画好的图。但「持有钥匙 j 才能进房间 j」就是一条有向边:从当前房间指向 j。顶点是房间,出边写在 rooms[i] 里。0 号房是唯一的入口。没有钥匙指向的房间,永远进不去。
缺的信息是「哪些房间我已经有资格进入」。没有 visited,拿到两把指向同一间房的钥匙会重复进;不从 0 出发而从每间房各走一次,等于假装所有门都开着——主例碰巧能对,但 [[1,3],[3,0,1],[2],[0]] 里房间 2 只装了自己的钥匙,从 0 走遍 0、1、3 也拿不到 2,必须返回 false。
从缺口反推。用栈或队列记下「已经进入、钥匙还没掏完」的房间。一开始只有 0 合法,visited[0]=true,count=1。弹出当前房间,遍历 rooms[cur] 里每把钥匙:目标还没进过,就标记、count+1、入栈。钥匙指向已访问的房间,直接丢掉。DFS 和 BFS 在这里只差出栈还是出队,覆盖集合相同。
用手走主例。进入房间 0,entered=[0],掏出钥匙 1。进入房间 1,entered=[0,1],掏出钥匙 2。进入房间 2,entered=[0,1,2],掏出钥匙 3。进入房间 3,房间是空的,没有新钥匙。四间都进过,count==4,返回 true。演示场景五帧就是这条链,最后一帧标 4/4。
循环结束时 count < n,就是有房间不在 0 的可达分量里。每间房、每把钥匙各处理一次,时间 O(V+E);栈最深时空间 O(V)。不要把「房间列表非空」当成可达:空房间可以是终点,主例的 3 就是。也不要漏掉钥匙指向自己——自环不提供新房间,visited 会挡住。
为什么必须从 0 出发0 是唯一没锁的门。从别的房间起走,等于作弊开门。全部房间可达,当且仅当 0 的有向可达集合覆盖 0..n−1。
Go:DFS 标记可达
func canVisitAllRooms(rooms [][]int) bool {n := len(rooms)visited := make([]bool, n)stack := []int{0}visited[0] = truecount := 1for len(stack) > 0 {cur := stack[len(stack)-1]; stack = stack[:len(stack)-1]for _, key := range rooms[cur] {if !visited[key] {visited[key] = truecount++stack = append(stack, key)}}}return count == n}
1栈里只放已经走进的房间。起点只能是 0,visited[0] 先打上,count 从 1 起。
2rooms[cur] 的每个 key 是一条出边。未访问才入栈,避免同一间房进两次。
3count 在第一次进入时加,不是每掏一把钥匙加一次。结束时和 n 比,缺一间就是 false。
总结
钥匙是有向边。从 0 做 DFS/BFS,走进的房间数等于 n 才 true。主例 0→1→2→3。
- 不能按房间号扫:没钥匙的门进不去。主例是链,反例是房间 2 自锁。
- visited 记的是「已进入」,不是「见过钥匙」。count 只在第一次进入时加。
- DFS 与 BFS 覆盖集合相同;本题不要求最短,用栈即可。