864. Shortest Path to Get All Keys #
Problem #
We are given a 2-dimensional grid. "." is an empty cell, "#" is a wall, "@" is the starting point, ("a", "b", …) are keys, and ("A", "B", …) are locks.
We start at the starting point, and one move consists of walking one space in one of the 4 cardinal directions. We cannot walk outside the grid, or walk into a wall. If we walk over a key, we pick it up. We can’t walk over a lock unless we have the corresponding key.
For some 1 <= K <= 6, there is exactly one lowercase and one uppercase letter of the first K letters of the English alphabet in the grid. This means that there is exactly one key for each lock, and one lock for each key; and also that the letters used to represent the keys and locks were chosen in the same order as the English alphabet.
Return the lowest number of moves to acquire all keys. If it’s impossible, return -1.
Example 1:
Input: ["@.a.#","###.#","b.A.B"]
Output: 8
Example 2:
Input: ["@..aA","..B#.","....b"]
Output: 6
Note:
1 <= grid.length <= 301 <= grid[0].length <= 30grid[i][j]contains only'.','#','@','a'-'f'and'A'-'F'- The number of keys is in
[1, 6]. Each key has a different letter and opens exactly one lock.
Problem Summary #
Given a two-dimensional grid. “.” represents an empty room, “#” represents a wall, “@” is the starting point, (“a”, “b”, …) represent keys, and (“A”, “B”, …) represent locks.
We start from the starting point, and one move means walking one unit in one of the four cardinal directions. We cannot walk outside the grid, nor can we pass through a wall. If we pass over a key, we pick it up. Unless we have the corresponding key, we cannot pass through a lock.
Assume K is the number of keys/locks and satisfies 1 <= K <= 6. The first K letters of the alphabet each have one corresponding lowercase and one uppercase letter in the grid. In other words, each lock has a unique corresponding key, and each key also has a unique corresponding lock. In addition, the letters representing keys and locks are corresponding uppercase/lowercase pairs and are arranged in alphabetical order.
Return the minimum number of moves required to obtain all keys. If it is impossible to obtain all keys, return -1.
Notes:
- 1 <= grid.length <= 30
- 1 <= grid[0].length <= 30
- grid[i][j] contains only ‘.’, ‘#’, ‘@’, ‘a’-‘f’ and ‘A’-‘F’
- The number of keys is in the range [1, 6]. Each key corresponds to a different letter and opens exactly one corresponding lock.
Solution Ideas #
- Given a map with keys and locks, locks cannot be passed through without keys. Starting from @, ask for the minimum number of steps needed to finally obtain all keys.
- This problem can be solved with BFS. Since there are several types of keys, the visited array needs 3 dimensions: one for the x-coordinate, one for the y-coordinate, and the last for the current state of obtained keys. Each key has two states: obtained and not obtained. The problem states that there are at most 6 keys, so the maximum number of combinations is 2^6 = 64 states. Use the binary bits of a decimal number to compress these states, where each bit represents whether a key has been obtained. Since the key state can be compressed, the x and y coordinates can actually be compressed into this number as well. The number stored in BFS is the state of coordinates + key state. During BFS traversal, use the visited array to filter out cases that have already been visited, ensuring that the path found is the shortest. The remaining cases are simply checking the state of locks, whether they can be passed through, and checking the key acquisition state.
- I am not sure whether this problem can be solved with DFS. I implemented one version, but it timed out on case 18 / 35. For the specific case, see the first case in the test file.
Code #
package leetcode
import (
"math"
"strings"
)
// Solution 1: BFS, use state compression to filter states
func shortestPathAllKeys(grid []string) int {
if len(grid) == 0 {
return 0
}
board, visited, startx, starty, res, fullKeys := make([][]byte, len(grid)), make([][][]bool, len(grid)), 0, 0, 0, 0
for i := 0; i < len(grid); i++ {
board[i] = make([]byte, len(grid[0]))
}
for i, g := range grid {
board[i] = []byte(g)
for _, v := range g {
if v == 'a' || v == 'b' || v == 'c' || v == 'd' || v == 'e' || v == 'f' {
fullKeys |= (1 << uint(v-'a'))
}
}
if strings.Contains(g, "@") {
startx, starty = i, strings.Index(g, "@")
}
}
for i := 0; i < len(visited); i++ {
visited[i] = make([][]bool, len(board[0]))
}
for i := 0; i < len(board); i++ {
for j := 0; j < len(board[0]); j++ {
visited[i][j] = make([]bool, 64)
}
}
queue := []int{}
queue = append(queue, (starty<<16)|(startx<<8))
visited[startx][starty][0] = true
for len(queue) != 0 {
qLen := len(queue)
for i := 0; i < qLen; i++ {
state := queue[0]
queue = queue[1:]
starty, startx = state>>16, (state>>8)&0xFF
keys := state & 0xFF
if keys == fullKeys {
return res
}
for i := 0; i < 4; i++ {
newState := keys
nx := startx + dir[i][0]
ny := starty + dir[i][1]
if !isInBoard(board, nx, ny) {
continue
}
if board[nx][ny] == '#' {
continue
}
flag, canThroughLock := keys&(1<<(board[nx][ny]-'A')), false
if flag != 0 {
canThroughLock = true
}
if isLock(board, nx, ny) && !canThroughLock {
continue
}
if isKey(board, nx, ny) {
newState |= (1 << (board[nx][ny] - 'a'))
}
if visited[nx][ny][newState] {
continue
}
queue = append(queue, (ny<<16)|(nx<<8)|newState)
visited[nx][ny][newState] = true
}
}
res++
}
return -1
}
// Solution 2: DFS, but it times out because the pruning condition is not strong enough
func shortestPathAllKeys1(grid []string) int {
if len(grid) == 0 {
return 0
}
board, visited, startx, starty, res, fullKeys := make([][]byte, len(grid)), make([][][]bool, len(grid)), 0, 0, math.MaxInt64, 0
for i := 0; i < len(grid); i++ {
board[i] = make([]byte, len(grid[0]))
}
for i, g := range grid {
board[i] = []byte(g)
for _, v := range g {
if v == 'a' || v == 'b' || v == 'c' || v == 'd' || v == 'e' || v == 'f' {
fullKeys |= (1 << uint(v-'a'))
}
}
if strings.Contains(g, "@") {
startx, starty = i, strings.Index(g, "@")
}
}
for i := 0; i < len(visited); i++ {
visited[i] = make([][]bool, len(board[0]))
}
for i := 0; i < len(board); i++ {
for j := 0; j < len(board[0]); j++ {
visited[i][j] = make([]bool, 64)
}
}
searchKeys(board, &visited, fullKeys, 0, (starty<<16)|(startx<<8), &res, []int{})
if res == math.MaxInt64 {
return -1
}
return res - 1
}
func searchKeys(board [][]byte, visited *[][][]bool, fullKeys, step, state int, res *int, path []int) {
y, x := state>>16, (state>>8)&0xFF
keys := state & 0xFF
if keys == fullKeys {
*res = min(*res, step)
return
}
flag, canThroughLock := keys&(1<<(board[x][y]-'A')), false
if flag != 0 {
canThroughLock = true
}
newState := keys
//fmt.Printf("x = %v y = %v fullKeys = %v keys = %v step = %v res = %v path = %v state = %v\n", x, y, fullKeys, keys, step, *res, path, state)
if (board[x][y] != '#' && !isLock(board, x, y)) || (isLock(board, x, y) && canThroughLock) {
if isKey(board, x, y) {
newState |= (1 << uint(board[x][y]-'a'))
}
(*visited)[x][y][newState] = true
path = append(path, x)
path = append(path, y)
for i := 0; i < 4; i++ {
nx := x + dir[i][0]
ny := y + dir[i][1]
if isInBoard(board, nx, ny) && !(*visited)[nx][ny][newState] {
searchKeys(board, visited, fullKeys, step+1, (ny<<16)|(nx<<8)|newState, res, path)
}
}
(*visited)[x][y][keys] = false
path = path[:len(path)-1]
path = path[:len(path)-1]
}
}
func isLock(board [][]byte, x, y int) bool {
if (board[x][y] == 'A') || (board[x][y] == 'B') ||
(board[x][y] == 'C') || (board[x][y] == 'D') ||
(board[x][y] == 'E') || (board[x][y] == 'F') {
return true
}
return false
}
func isKey(board [][]byte, x, y int) bool {
if (board[x][y] == 'a') || (board[x][y] == 'b') ||
(board[x][y] == 'c') || (board[x][y] == 'd') ||
(board[x][y] == 'e') || (board[x][y] == 'f') {
return true
}
return false
}