# Libraries
library(tidyverse)
# Read input from file
<- read_lines("../input/day17.txt", skip_empty_rows = TRUE) input
Day 17
Advent of Code: Worked Solutions
Setup
Part 1
Convert text input to a set of 2D coordinates (stored as complex numbers) and their costs:
<- input |>
vtx enframe(name = "row") |>
mutate(value = str_split(value, "")) |>
unnest_longer(value, indices_to = "col") |>
transmute(
id = row_number(),
z = complex(real = col, imaginary = row),
cost = as.integer(value)
)
<- pull(vtx, z)
z <- pull(vtx, cost)
cost
<- length(z) nmax
Define a set of helper functions needed for the pathfinding algorithm:
# Sort a list of lists by the value at the given/named index of the inner lists
<- \(x, idx) x[order(sapply(x, '[[', idx))]
nested_sort <- partial(nested_sort, idx = "priority")
psort
# Compute the manhattan distance between any two vertices
<- \(a, b) abs(Re(a - b)) + abs(Im(a - b))
manhattan_dist
# Pull a vertex's neighbors along the given axis (Re/Im) within a set dist range
<- function(vtx, axis = c("Re", "Im"), dmin, dmax) {
neighbors <- c(-dmax:-dmin, dmin:dmax)
rng <- vtx + (rng * complex(real = axis == "Re", imaginary = axis == "Im"))
nbr keep(nbr, ~ .x %in% z)
}
# Compute the cost of moving in a straight line between two vertices
<- function(source, target) {
move_cost <- complex(
path_indices real = seq(Re(source), Re(target)),
imaginary = seq(Im(source), Im(target))
|>
) discard(~ .x == source) |>
match(z)
sum(cost[path_indices])
}
A* pathfinding implementation (with much help from Reddit on use of complex numbers and storing the turns as part of the state):
- https://en.wikipedia.org/wiki/A*_search_algorithm
- https://www.redblobgames.com/pathfinding/a-star/introduction.html
- https://www.reddit.com/r/adventofcode/comments/18khohi/comment/kdrkivy/
<- function(start, goal, h, dmin, dmax) {
a_star
# Current set of discovered nodes awaitng further investigation
<- list(
frontier list(id = start, axis = 'Re', priority = 0),
list(id = start, axis = 'Im', priority = 0)
)
# For each vertex, the immediately preceeding vertex on its cheapest path
<- rep(list(lst(Re = NA, Im = NA)), nmax)
came_from
# Tracks the current min cost to get from 'start' node to node n
<- rep(list(lst(Re = Inf, Im = Inf)), nmax)
cost_so_far $Re <- 0
cost_so_far[[start]]$Im <- 0
cost_so_far[[start]]
while (length(frontier) > 0) {
<- frontier[[1]]$id
cur_id <- z[[cur_id]]
cur_z <- frontier[[1]]$axis
cur_axis <- case_match(cur_axis, 'Re' ~ 'Im', 'Im' ~ 'Re')
nxt_axis <- tail(frontier, -1)
frontier
# Return info about best path to goal
if (cur_id == goal) return(cost_so_far[[cur_id]][[cur_axis]])
# If this path is better than the current record, then replace it
for (nxt_z in neighbors(cur_z, cur_axis, dmin, dmax)) {
<- match(nxt_z, z)
nxt_id <- cost_so_far[[cur_id]][[cur_axis]] + move_cost(cur_z, nxt_z)
tentative_cost
if (tentative_cost < cost_so_far[[nxt_id]][[nxt_axis]]) {
<- cur_id
came_from[[nxt_id]][[nxt_axis]] <- tentative_cost
cost_so_far[[nxt_id]][[nxt_axis]]
<- psort(c(frontier, list(list(
frontier id = nxt_id,
axis = nxt_axis,
priority = tentative_cost + h(nxt_z, z[[goal]])
))))
}
}
} }
Run on puzzle input:
a_star(1, nmax, h = manhattan_dist, dmin = 1, dmax = 3)
[1] 847
Part 2
Re-run on puzzle input with modified min/max distance values passed to the neighbors function:
a_star(1, nmax, h = manhattan_dist, dmin = 4, dmax = 10)
[1] 997