library(tidyverse)
library(igraph)
library(memoise)
Day 23
Advent of Code: Worked Solutions
Setup
Import libraries:
Read and parse text input from file:
<- read_lines("../input/day23.txt", skip = 2, n_max = 2) |>
input str_extract_all("[A-Z]") |>
pmap(~ c(..1, ..2))
Part 1
We represent every state of the board as a string, which allows us to concisely store board and to leverage regex in our computations.
<- str_flatten(c(' ', rep(c(' ', '#'), 4), ' '))
halls <- map_chr(input, str_flatten)
rooms <- str_c(halls, str_flatten(rooms, collapse = ","), sep = ":") board
For a given arrangement of free & blocked spaces in the hallway (irrelavant of the occupants of the blocked spaces and the status of the rooms), determine the set of legal moves from each room to a space in the hall:
# Extract the free spaces around each room and convert to a list of indices:
<- function(halls) {
.room_moves |>
halls str_locate_all('( *# *)+') |>
pluck(1) |>
as_tibble() |>
transmute(
room_idx = map2(start, end, \(start, end) {
keep(c(0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 0)[start:end], ~ .x > 0)
}),hall_idx = map2(start, end, \(start, end) {
keep(c(1, 2, 0, 4, 0, 6, 0, 8, 0, 10, 11)[start:end], ~ .x > 0)
})|>
) unnest_longer(room_idx) |>
pull(hall_idx)
}
# Memoize for performance: only 128 configurations in total (7 choose k)
<- memoize(.room_moves)
.room_moves <- \(x) .room_moves(str_replace_all(x, "[A-Z]", "X")) room_moves
The list of unblocked moves from the hall to a room is narrower, since an amphipod can only move into its final room:
<- function(halls) {
hall_moves map(1:4, \(room_num) {
<- str_sub(halls, 1L, 2 * room_num)
str_l <- str_sub(halls, 2 * room_num + 2, -1L)
str_r
<- str_locate(str_l, str_c(LETTERS[room_num], "[# ]*$"))[,"start"]
idx_l <- str_locate(str_r, str_c("^[# ]*", LETTERS[room_num]))[,"end"]
idx_r <- idx_r + 2 * room_num + 1
idx_r
unname(discard(c(idx_l, idx_r), is.na))
})
}
<- memoize(hall_moves) hall_moves
Define helper functions to compute the cost of moving between two locations on board:
<- function(hall, room, room_size) {
num_spaces <- room_size + 1
n
<- ceiling((room - 12) / n) * 2 + 1
room_entry <- (room - 13) %% n + 1
room_depth abs(hall - room_entry) + room_depth
}
<- memoize(num_spaces)
num_spaces
<- function(from, to, chr, room_size) {
cost <- match(chr, LETTERS)
idx 10^(idx - 1) * num_spaces(min(from, to), max(from, to), room_size)
}
<- memoize(cost) cost
Define a helper function to convert a nested list of room indices and hall indices along with information about the current board state into to a new board configuration (as a string):
<- function(moves, spaces, vec, room_size, dir = c("in", "out")) {
to_board
<- which(c("in", "out") == dir)
n
map2(
moves,0:3 * (room_size + 1) + 11) + spaces + n,
(
\(hall_set, room_idx) {map_chr(hall_set, \(hall_idx) {
<- c(room_idx, hall_idx)[n]
to <- c(room_idx, hall_idx)[-n]
from str_c(
str_flatten(replace(vec, c(from, to), c(" ", vec[from]))),
cost(from, to, vec[from], room_size),
sep = ";"
)
})
}|>
) unlist()
}
From a given hall/room configuration, get a list of valid next moves:
<- \(x) str_count(x, " ")
room_spaces <- \(x) str_detect(x, c("[BCD]", "[ACD]", "[ABD]", "[ABC]"))
room_has_invalid
<- function(board, room_size) {
next_moves <- str_split_1(board, "")
vec <- str_split_1(board, ":")
board <- board[1]
halls <- board[2] |> str_split_1(",")
rooms
# Determine which amphipods can move OUT of their room in the next step:
<- room_spaces(rooms)
room_spaces <- room_has_invalid(rooms)
room_has_invalid
# Get all valid hall-to-room moves first, then room-to-hall if none available.
<- hall_moves(halls)
hall_to_room <- list(numeric(0))
hall_to_room[room_has_invalid]
# If there are any hall-to-room moves, that's the only path we should take.
if (any(map_int(hall_to_room, length) > 0)) {
to_board(hall_to_room, room_spaces, vec, room_size, "in")
else {
} <- room_moves(halls)
room_to_hall !room_has_invalid] <- list(numeric(0))
room_to_hall[to_board(room_to_hall, room_spaces, vec, room_size, "out")
} }
Create a queue to explore every board state. Once the connections between all board states has been established, convert to a graph and find the shortest distance between the start and end, weighted by movement cost:
<- function(start, end) {
get_dist
<- start |>
room_size str_split_1(":") |>
pluck(2) |>
str_split_1(",") |>
pluck(1) |>
str_length()
<- start
queue <- list()
steps
<- 1
i
while(i <= length(queue)) {
<- queue[i]
cur <- next_moves(cur, room_size)
nxt <- list(nxt)
steps[cur] <- nxt |> str_split(';') |> map_chr(~ .x[1])
nxt <- c(queue, setdiff(nxt, queue))
queue <- i + 1
i
}
<- steps |>
g enframe(name = "V1") |>
unnest_longer(value) |>
separate_wider_delim(value, delim = ";", names = c("V2", "weight")) |>
mutate(weight = as.numeric(weight)) |>
graph_from_data_frame(directed = TRUE)
distances(g, v = start, to = end)[1, 1]
}
Run on puzzle input:
get_dist(board, " # # # # :AA,BB,CC,DD")
Part 2
Manually insert the new lines:
#D#C#B#A#
#D#B#A#C#
<- map2(
rooms
rooms,c("DD", "CB", "BA", "AC"),
~ str_split_1(.x, "") |>
str_flatten(collapse = .y)
)
<- str_c(halls, str_flatten(rooms, collapse = ","), sep = ":") board
Re-run on the new input to get the new distance:
get_dist(board, " # # # # :AAAA,BBBB,CCCC,DDDD")