# Libraries
library(tidyverse)
library(igraph)
# Read input from file
<- read_lines("../input/day21.txt") input
Day 21
Advent of Code: Worked Solutions
Setup
Part 1
Define functions calculate minimum distances between keys on a keypad:
# Helper function to convert pairs of adjacent keys to their directions
<- function(keys, rows, cols) {
keypad_to_df <- tibble(key = keys, row = rows, col = cols)
df
cross_join(df, df, suffix = c("_from", "_to")) |>
filter(abs(row_to - row_from) + abs(col_to - col_from) == 1) |>
mutate(dir = case_when(
< col_to ~ ">",
col_from > col_to ~ "<",
col_from < row_to ~ "^",
row_from > row_to ~ "v"
row_from
))
}
# Convert a keypad (set of keys and their coordinates) to a graph
<- function(keys, rows, cols) {
keypad_to_graph <- keypad_to_df(keys, rows, cols)
df <- df |>
g transmute(from = key_from, to = key_to) |>
graph_from_data_frame(vertices = keys)
<- function(from, to) {
f |>
df filter(key_from == from & key_to == to) |>
pull(dir)
}list("df" = df, "g" = g, "f" = f)
}
# Return all shortests paths between two keys on a keypad
<- function(keypad, key_from, key_to) {
keypad_paths all_shortest_paths(keypad$g, from = key_from, to = key_to)$vpaths |>
map_chr(\(x) {
<- map2_chr(
dirs head(names(x), -1),
head(lead(names(x)), -1),
$f(dir_from, dir_to)
\(dir_from, dir_to) keypad
)str_c(c(dirs, "A"), collapse = "")
})
}
# Define the numeric keypad
<- c("A", 0:9)
keys_num <- case_match(keys_num,
rows_num c("A", "0") ~ 1,
c("1", "2", "3") ~ 2,
c("4", "5", "6") ~ 3,
c("7", "8", "9") ~ 4
)<- case_match(keys_num,
cols_num c("1", "4", "7") ~ 1,
c("0", "2", "5", "8") ~ 2,
c("A", "3", "6", "9") ~ 3
)<- keypad_to_graph(keys = keys_num, row = rows_num, col = cols_num)
keypad_num
# Define the directional keypad
<- c("<", "v", ">", "^", "A")
keys_dir <- case_match(keys_dir,
rows_dir c("<", "v", ">") ~ 1,
c("^", "A") ~ 2
)<- case_match(keys_dir,
cols_dir c("<") ~ 1,
c("v", "^") ~ 2,
c(">", "A") ~ 3
)<- keypad_to_graph(keys = keys_dir, row = rows_dir, col = cols_dir)
keypad_dir
# Recursively compute the minimum user input for a given input string
<- function(input_str, level = 0, max_level = 3) {
min_path if (level == max_level)
return(str_length(input_str))
<- if (level == 0) keypad_num else keypad_dir
keypad
<- str_split_1(input_str, "")
input_to <- lag(input_to, default = "A")
input_from <- map2_int(
steps
input_from,
input_to, keypad_paths(keypad, from, to) |>
\(from, to) map_int(min_path, level = level + 1, max_level = max_level) |>
min()
)sum(steps)
}
Run on puzzle input:
tibble(input) |>
mutate(
numeric_code = parse_number(input),
shortest_seq = map_int(input, min_path),
complexity = numeric_code * shortest_seq
|>
) pull(complexity) |>
sum()
[1] 157908
Part 2
Modify the min_path function to condense all inputs wherever possible so needless computation isn’t repeated:
<- function(input_str, level = 0, max_level = 3) {
min_path if (level == max_level)
return(str_length(input_str))
<- if (level == 0) keypad_num else keypad_dir
keypad
<- tibble(str = input_str) |>
df mutate(
id = row_number(),
key = map(str, ~ tibble(
input_to = str_split_1(.x, ""),
input_from = lag(input_to, default = "A")
))|>
) unnest(key)
<- df |>
steps distinct(input_from, input_to) |>
mutate(paths = map2(input_from, input_to, ~ keypad_paths(keypad, .x, .y))) |>
unnest(paths) |>
mutate(len = min_path(paths, level = level + 1, max_level = max_level)) |>
slice_min(len, by = c(input_from, input_to), with_ties = FALSE)
|>
df left_join(steps, join_by(input_from, input_to)) |>
summarize(steps = sum(len), .by = c(id, str)) |>
pull(steps)
}
Re-run puzzle input with a max level of 26:
tibble(input) |>
mutate(
numeric_code = parse_number(input),
shortest_seq = map_dbl(input, min_path, max_level = 26),
complexity = numeric_code * shortest_seq
|>
) pull(complexity) |>
sum() |>
format(scientific = FALSE)
[1] "196910339808654"