library(tidyverse)
library(rlang)
library(ggdark)
library(khroma)
library(showtext)
library(scales)
library(tidygraph)
library(ggraph)
font_add_google(name = "Mountains of Christmas", family = "christmas")
font_add(family = "Noto Emoji", regular = file.path(font_paths()[2], "NotoEmoji-VariableFont_wght.ttf"))
showtext_auto()
theme_set(dark_theme_gray() +
theme(title = element_text(family = "christmas", size = 20)))
::knit_hooks$set(crop = knitr::hook_pdfcrop) knitr
Day 12
Part 1
This is a doozy. I’m not already familiar with shortest path algorithms, but I found one called Dijkstra’s Algorithm that I try to implement here.
Read in the data and convert to numeric matrix
<- read_lines("2022-12-12_assets/input.txt") lines
<- c(1, 1:26, 26)
height_map names(height_map) <- c("S", letters, "E")
|>
lines str_split(pattern = "") -> height_lines
|>
height_lines map(~height_map[.x]) |>
map(~setNames(.x, NULL)) |>
reduce(rbind) -> height_mat
|>
height_lines reduce(rbind)->height_letters
Get the indices of the start and end points.
<- floor(which(height_letters == "S") /nrow(height_letters)) + 1
start_col <- which(height_letters == "S") %% nrow(height_letters) start_row
<- floor(which(height_letters == "E") /nrow(height_letters)) + 1
end_col <- which(height_letters == "E") %% nrow(height_letters) end_row
Just for fun, a map of the terrain.
|>
height_mat as_tibble(.name_repair = "unique") |>
mutate(y = 1:n()) |>
pivot_longer(cols = -y, names_to = "x") |>
mutate(x = str_remove(x, "\\.+") |>
as.numeric()) |>
ggplot(aes(x, y))+
geom_raster(aes(fill = value))+
geom_point(x = start_col, y = start_row)+
annotate(x = end_col,
y = end_row,
label = "X",
color = "black",
geom = "text",
family = "christmas",
size = 5)+
scale_fill_gradientn(colours = terrain.colors(10),
guide = "none")+
scale_x_continuous(expand = expansion(mult = 0.01))+
scale_y_continuous(expand = expansion(mult = 0.01))+
labs(x = "longitude",
y = "latitude",
title = "the terrain")+
coord_fixed()
<- c(start_row, start_col) start_idx
The implementation
I did not write these out in the order that they appear.
<- list(rbind(c(start_row, start_col))) path_states
I need a function to generate potential steps given an input that do not go off the map.
= path_states[[1]]
position <- function(position, nrows, ncols){
make_steps <- rbind(c(-1,0),
stepper c(1,0),
c(0,-1),
c(0,1))
<- apply(stepper,
next_steps 1,
as_function(~.x + position)) |>
t()
# the steps can't go off the map
<- next_steps[,1] > 0 & next_steps[,1] <= nrows
row_ok <- next_steps[,2] > 0 & next_steps[,2] <= ncols
col_ok = row_ok & col_ok
both_ok <- next_steps[both_ok,]
next_steps return(next_steps)
}<- make_steps(position, nrows = nrow(height_mat), ncols = ncol(height_mat))
steps steps
[,1] [,2]
[1,] 20 1
[2,] 22 1
[3,] 21 2
I’ll also need unique ids for every location to filter out previously visited locations, so I’ll just use the row and column concatenated.
<- function(steps){
make_node_ids <- apply(steps, 1, as_function(~str_c(.x, collapse = ",")))
ids return(ids)
}
<- make_node_ids(steps)
step_ids step_ids
[1] "20,1" "22,1" "21,2"
Only steps where the step to is no more than 1 greater than the current position.
<- function(position, steps, traversal){
score_steps <- traversal[position[1], position[2]]
current <- apply(steps, 1, as_function(~height_mat[.x[1], .x[2]]))
potential <- potential - current
score return(score)
}
score_steps(position, steps, height_mat)
[1] 0 0 1
One function to roll up the previous
- Generate valid steps
- Scores the steps
- Return valid, possible steps.
<- function(state, height_mat){
make_step_possibilitites <- state
curr_path <- curr_path[nrow(curr_path), ]
curr_pos <- make_steps(curr_pos, ncols = ncol(height_mat), nrows = nrow(height_mat))
next_steps <- score_steps(curr_pos, next_steps, height_mat)
next_scores <- rbind(next_steps[next_scores <= 1, ])
next_steps return(next_steps)
}
For every path in the current set of paths, prune the step possibilities so that only one path can move to a node.
<- function(path_states, step_possibilities){
prune_steps <- map(step_possibilities, make_node_ids)
step_ids # we only need to compare each set of step possibilities once,
# hence i in 1:n-1 and j in 2:n
for(idx in 1:(length(path_states)-1)) {
for(jdx in (idx+1):length(path_states)) {
if(any(step_ids[[idx]] %in% step_ids[[jdx]])) {
# This is gratuitous, on second thought, but
# if path i is shorter, it gets the next step
# if path j is shorter, it gets the next step.
# if they're the same length, it doesn't matter, just give it to i
if(nrow(path_states[[idx]]) > nrow(path_states[[jdx]])) {
<- which(step_ids[[idx]] %in% step_ids[[jdx]])
i_rows <- rbind(step_possibilities[[idx]][-i_rows,])
step_possibilities[[idx]] else if(nrow(path_states[[jdx]]) > nrow(path_states[[idx]])) {
}<- which(step_ids[[jdx]] %in% step_ids[[idx]])
j_rows <- rbind(step_possibilities[[jdx]][-j_rows,])
step_possibilities[[jdx]] else{
}<- which(step_ids[[idx]] %in% step_ids[[jdx]])
i_rows <- rbind(step_possibilities[[idx]][-i_rows,])
step_possibilities[[idx]]
}
}
}
}return(step_possibilities)
}
We also need to prune any steps into nodes that have already been visited.
<- function(steps, visited){
prune_visited <- make_node_ids(steps)
node_ids <- rbind(steps[!node_ids %in% visited, ])
steps return(steps)
}
I need a function that takes in the preceding path states and the current (pruned) future steps. Rather than update the current paths directly, I pre-compile a list the length of the total next steps. Then, I add the next steps to their appropriate paths. I also prune paths which had no next steps at this stage.
<- function(path_states, step_possibilities){
make_new_paths <- map(step_possibilities, nrow) |> simplify()
n_steps <- sum(n_steps)
total_steps <- vector(mode = "list", length = total_steps)
new_path_states <- 1
newstate_idx for(idx in seq_along(path_states)){
for(jdx in seq_along(step_possibilities[[idx]][,1])){
<- rbind(path_states[[idx]], step_possibilities[[idx]][jdx,])
new_path <- new_path
new_path_states[[newstate_idx]] = newstate_idx+1
newstate_idx
}
}> 0]
new_path_states[n_steps return(new_path_states)
}
Finally, we need to know when we finished!
<- function(steps, end_row, end_col){
check_winner <- apply(steps, 1, as_function(~.x[1] == end_row & .x[2] == end_col))
winners return(winners)
}
<- list(rbind(c(start_row, start_col)))
path_states # This is just for book keeping
<- length(path_states)
n_paths <- make_node_ids(path_states[[1]])
visited <- map(path_states, ~make_step_possibilitites(.x, height_mat))
step_possibilities = F
winner # Hold onto your butts!
while(!winner){
if(length(step_possibilities) > 1){
<- prune_steps(path_states, step_possibilities)
step_possibilities <- map(step_possibilities, ~check_winner(.x, end_row, end_col))
step_winners if(any(map(step_winners, any) |> simplify())){
= T
winner break
}<- map(step_possibilities, ~prune_visited(.x, visited))
step_possibilities <- make_new_paths(path_states, step_possibilities)
new_path_states <- c(n_paths, length(new_path_states))
n_paths <- c(visited, map(step_possibilities, make_node_ids) |> simplify())
visited else{
}<- map(step_possibilities, ~prune_visited(.x, visited))
step_possibilities <- make_new_paths(path_states, step_possibilities)
new_path_states <- c(n_paths, length(new_path_states))
n_paths <- c(visited, map(step_possibilities, make_node_ids) |> simplify())
visited
}<- new_path_states
path_states <- map(path_states, ~make_step_possibilitites(.x, height_mat))
step_possibilities }
The example answer does not include the end state, and all paths right now have the same length, so
nrow(path_states[[1]])
[1] 462
But I want to make a plot, so here you go:
<- which(map(step_winners, any) |> simplify())
winning_path_idx <- path_states[[winning_path_idx]]
winning_path <- step_possibilities[[winning_path_idx]][step_winners[[winning_path_idx]], ]
winning_step <- rbind(winning_path, winning_step)
winning_path <- data.frame(winning_path) |> mutate(x = X2, y = X1) winning_path_df
|>
height_mat as_tibble(.name_repair = "unique") |>
mutate(y = 1:n()) |>
pivot_longer(cols = -y, names_to = "x") |>
mutate(x = str_remove(x, "\\.+") |>
as.numeric()) |>
ggplot(aes(x, y))+
geom_raster(aes(fill = value))+
geom_point(x = start_col, y = start_row)+
geom_path(data = winning_path_df,
color = "black",
linetype = 2)+
annotate(x = end_col,
y = end_row,
label = "X",
color = "black",
geom = "text",
family = "christmas",
size = 5)+
scale_fill_gradientn(colours = terrain.colors(10),
guide = "none")+
scale_x_continuous(expand = expansion(mult = 0.01))+
scale_y_continuous(expand = expansion(mult = 0.01))+
labs(x = "longitude",
y = "latitude",
title = "winning path")+
coord_fixed()
Huh!
tibble(n_paths = n_paths) |>
mutate(n = 1:n()) ->n_path_df
|>
n_path_df ggplot(aes(n, n_paths))+
geom_line(linewidth = 1)+
labs(x = "time step",
y = "current possible paths",
title = "Possible paths over time")
|>
winning_path_df mutate(n = 1:n()) |>
left_join(n_path_df) -> winning_path_df
Joining, by = "n"
|>
height_mat as_tibble(.name_repair = "unique") |>
mutate(y = 1:n()) |>
pivot_longer(cols = -y, names_to = "x") |>
mutate(x = str_remove(x, "\\.+") |>
as.numeric()) |>
ggplot(aes(x, y))+
geom_raster(aes(fill = value))+
geom_point(x = start_col, y = start_row)+
geom_path(data = winning_path_df,
aes(color = n_paths),
linewidth= 2)+
annotate(x = end_col,
y = end_row,
label = "X",
color = "black",
geom = "text",
family = "christmas",
size = 5)+
scale_fill_grayC(guide = "none", reverse = T)+
scale_color_hawaii()+
scale_x_continuous(expand = expansion(mult = 0.01))+
scale_y_continuous(expand = expansion(mult = 0.01))+
labs(x = "longitude",
y = "latitude",
title = "path quantum state")+
coord_fixed()
Warning: Removed 1 row containing missing values (`geom_path()`).
Part 2
Ok, just need to abstractify the previous answer.
<- function(path_states, step_possibilities){
prune_steps2 <- map(step_possibilities, make_node_ids)
step_ids # we only need to compare each set of step possibilities once,
# hence i in 1:n-1 and j in 2:n
for(idx in 1:(length(path_states)-1)) {
for(jdx in (idx+1):length(path_states)) {
if(any(step_ids[[idx]] %in% step_ids[[jdx]])) {
# This is gratuitous, on second thought, but
# if path i is shorter, it gets the next step
# if path j is shorter, it gets the next step.
# if they're the same length, it doesn't matter, just give it to i
if(nrow(path_states[[idx]]) > nrow(path_states[[jdx]])) {
<- which(step_ids[[idx]] %in% step_ids[[jdx]])
i_rows <- rbind(step_possibilities[[idx]][-i_rows,])
step_possibilities[[idx]] else if(nrow(path_states[[jdx]]) > nrow(path_states[[idx]])) {
}<- which(step_ids[[jdx]] %in% step_ids[[idx]])
j_rows <- rbind(step_possibilities[[jdx]][-j_rows,])
step_possibilities[[jdx]] else{
}<- which(step_ids[[idx]] %in% step_ids[[jdx]])
i_rows <- rbind(step_possibilities[[idx]][-i_rows,])
step_possibilities[[idx]]
}
}
}
}return(step_possibilities)
}
<- function(path_states, end_row, end_col, height_mat){
find_winner #path_states <- list(rbind(c(start_row, start_col)))
# This is just for book keeping
<- length(path_states)
n_paths <- make_node_ids(path_states[[1]])
visited <- map(path_states, ~make_step_possibilitites(.x, height_mat))
step_possibilities = F
winner # Hold onto your butts!
while(!winner){
if(length(step_possibilities) > 1){
<- prune_steps2(path_states, step_possibilities)
step_possibilities <- map(step_possibilities, ~check_winner(.x, end_row, end_col))
step_winners if(any(map(step_winners, any) |> simplify())){
= T
winner break
}<- map(step_possibilities, ~prune_visited(.x, visited))
step_possibilities <- make_new_paths(path_states, step_possibilities)
new_path_states <- c(n_paths, new_path_states)
n_paths <- c(visited, map(step_possibilities, make_node_ids) |> simplify())
visited else{
}<- map(step_possibilities, ~prune_visited(.x, visited))
step_possibilities <- make_new_paths(path_states, step_possibilities)
new_path_states <- c(n_paths, new_path_states)
n_paths <- c(visited, map(step_possibilities, make_node_ids) |> simplify())
visited
}<- new_path_states
path_states <- map(path_states, ~make_step_possibilitites(.x, height_mat))
step_possibilities
}<- which(map(step_winners, any) |> simplify())
winning_path_idx <- path_states[[winning_path_idx]]
winning_path <- step_possibilities[[winning_path_idx]][step_winners[[winning_path_idx]], ]
winning_step <- rbind(winning_path, winning_step)
winning_path return(winning_path)
}
Ok, now just to get a list of all starting points, and map over them.
<- which(height_mat == 1, arr.ind = T) all_starts
nrow(all_starts)
[1] 1724
Ok, actually, I should just take all of these starting states as a path state!
<- vector(mode = "list", length = nrow(all_starts))
starts_as_states for(idx in seq_along(starts_as_states)){
<- rbind(all_starts[idx,])
starts_as_states[[idx]] }
<- find_winner(starts_as_states, end_row = end_row, end_col = end_col, height_mat) shortest
This isn’t the right answer :(
nrow(shortest)- 1
[1] 458