Skip to content

Commit 0eb7f5a

Browse files
committed
2023 days 23-25
1 parent 517a462 commit 0eb7f5a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+8634
-1313
lines changed

2023/R/day23.qmd

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
---
2+
title: "Day 23"
3+
date: 2023-12-23
4+
author:
5+
name: https://adventofcode.com/2023/day/23
6+
url: https://adventofcode.com/2023/day/23
7+
---
8+
9+
## Setup
10+
11+
```{r setup}
12+
13+
# Libraries
14+
library(tidyverse)
15+
library(igraph)
16+
17+
# Read input from file
18+
input <- read_lines("../input/day23.txt", skip_empty_rows = FALSE)
19+
20+
```
21+
22+
## Part 1
23+
24+
Convert text input to a directed graph:
25+
26+
```{r}
27+
28+
# Convert to a dataframe with IDs and coordinates per cell
29+
df <- input |>
30+
str_split("") |>
31+
enframe(name = "row") |>
32+
unnest_longer(value, indices_to = "col") |>
33+
mutate(id = row_number()) |>
34+
filter(value != '#') |>
35+
relocate(id, value, row, col)
36+
37+
# Compute the list of directed edges between cells
38+
edges <- df |>
39+
mutate(
40+
row_n = row - 1,
41+
row_s = row + 1,
42+
col_w = col - 1,
43+
col_e = col + 1
44+
) |>
45+
left_join(df, join_by(x$row_n == y$row, col), suffix = c("", "_n")) |>
46+
left_join(df, join_by(x$row_s == y$row, col), suffix = c("", "_s")) |>
47+
left_join(df, join_by(x$col_w == y$col, row), suffix = c("", "_w")) |>
48+
left_join(df, join_by(x$col_e == y$col, row), suffix = c("", "_e")) |>
49+
select(-starts_with(c("row", "col", "value_"))) |>
50+
pivot_longer(
51+
starts_with("id_"),
52+
names_to = "dir",
53+
values_to = "neighbor",
54+
names_prefix = "id_"
55+
) |>
56+
57+
# For slope tiles, remove any non-downhill neighbors
58+
filter(
59+
(value == "." & !is.na(neighbor)) |
60+
(value == "^" & dir == "n") |
61+
(value == "v" & dir == "s") |
62+
(value == "<" & dir == "w") |
63+
(value == ">" & dir == "e")
64+
) |>
65+
pmap(\(id, neighbor, ...) c(id, neighbor)) |>
66+
unlist()
67+
68+
# Convert to a graph
69+
g <- make_graph(edges)
70+
71+
```
72+
73+
Find the longest possible path from the start point to the end point:
74+
75+
```{r}
76+
77+
source <- min(df$id)
78+
target <- max(df$id)
79+
80+
max_hike <- function(g, from = source, to = target) {
81+
all_simple_paths(g, from, to) |>
82+
map_dbl(~ length(.x) - 1) |>
83+
sort(decreasing = TRUE) |>
84+
max()
85+
}
86+
87+
max_hike(g)
88+
89+
```
90+
91+
## Part 2
92+
93+
Convert to an undirected graph to remove the slope constraint:
94+
95+
```{r}
96+
97+
g <- as_undirected(g)
98+
V(g)$name <- V(g)
99+
100+
```
101+
102+
The graph is too large to simply run the hike length function again -- an overflow results.
103+
104+
Instead, we notice that the input maze consists of relatively few intersections. Most of the maze input is simple corridors with no path decisions. We can reduce the graph complexity/size by trimming away our non-choice verftices and converting the length of those paths to an edge weight.
105+
106+
```{r}
107+
108+
v_zero_edges <- names(which(degree(g) == 0))
109+
v_two_edges <- names(which(degree(g) == 2))
110+
v_nontwo_edges <- names(which(degree(g) != 2))
111+
112+
# Extract all corridor vertices
113+
g_corridors <- delete_vertices(g, v_nontwo_edges)
114+
corridors <- components(g_corridors)
115+
116+
# Determine which edges to add to replace the corridors and their weight
117+
new_weights <- corridors$csize + 1
118+
new_edges <- corridors$membership |>
119+
keep_at(names(which(degree(g_corridors) == 1))) |>
120+
enframe(name = "vtx", value = "group") |>
121+
mutate(vtx = map_chr(vtx, ~ setdiff(names(neighbors(g, .x)), v_two_edges))) |>
122+
summarize(edge = list(vtx), .by = group) |>
123+
arrange(group) |>
124+
pull(edge)
125+
126+
# Create a new graph without the corridor vertices, then add its new edges
127+
g_new <- reduce2(
128+
.x = new_edges,
129+
.y = new_weights,
130+
.f = \(g, e, w) add_edges(g, e, weight = w),
131+
.init = delete_vertices(g, c(v_zero_edges, v_two_edges))
132+
)
133+
134+
```
135+
136+
View a plot of the resulting simplified graph:
137+
138+
```{r}
139+
#| fig-height: 9
140+
#| fig-width: 10
141+
142+
vtx_labels <- g_new |>
143+
V() |>
144+
names() |>
145+
case_match(
146+
as.character(source) ~ "S",
147+
as.character(target) ~ "E",
148+
.default = ""
149+
)
150+
151+
plot(
152+
g_new,
153+
vertex.size = 8,
154+
vertex.label = vtx_labels,
155+
edge.label = E(g_new)$weight
156+
)
157+
158+
```
159+
160+
Compute all paths from the start to the end using our smaller graph:
161+
162+
```{r}
163+
164+
all_paths <- g_new |>
165+
all_simple_paths(as.character(source), as.character(target))
166+
167+
```
168+
169+
Using the edge weights of our graph, compute the total length of each path and select the longest:
170+
171+
```{r}
172+
173+
all_paths |>
174+
map(
175+
~ .x |>
176+
as_ids() |>
177+
rep(each = 2) |>
178+
head(-1) |>
179+
tail(-1) |>
180+
get_edge_ids(graph = g_new)
181+
) |>
182+
map(~ E(g_new)$weight[.x]) |>
183+
map_dbl(sum) |>
184+
max()
185+
186+
```
187+
188+

2023/R/day24.qmd

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
---
2+
title: "Day 24"
3+
date: 2023-12-24
4+
author:
5+
name: https://adventofcode.com/2023/day/24
6+
url: https://adventofcode.com/2023/day/24
7+
---
8+
9+
## Setup
10+
11+
```{r setup}
12+
13+
# Libraries
14+
library(tidyverse)
15+
library(unglue)
16+
17+
# Read input from file
18+
input <- read_lines("../input/day24.txt", skip_empty_rows = FALSE)
19+
20+
```
21+
22+
## Part 1
23+
24+
Convert text input to structured data:
25+
26+
```{r}
27+
28+
bound_min <- 200000000000000
29+
bound_max <- 400000000000000
30+
31+
df <- input |>
32+
unglue_data("{px}, {py}, {pz} @ {vx}, {vy}, {vz}", convert = TRUE) |>
33+
mutate(id = row_number(), .before = everything())
34+
35+
vecs_2d <- df |>
36+
transmute(
37+
id,
38+
p = pmap(lst(px, py), ~ matrix(c(..1, ..2), ncol = 1)),
39+
v = pmap(lst(vx, vy), ~ matrix(c(..1, ..2), ncol = 1))
40+
)
41+
42+
vecs_3d <- df |>
43+
transmute(
44+
id,
45+
p = pmap(lst(px, py, pz), ~ matrix(c(..1, ..2, ..3), ncol = 1)),
46+
v = pmap(lst(vx, vy, vz), ~ matrix(c(..1, ..2, ..3), ncol = 1))
47+
)
48+
49+
```
50+
51+
The position $\vec a$ of a hailstone at any given time $t$ can be written in the format:
52+
53+
$$\vec vt + \vec p$$
54+
55+
The intersection of the paths of any two given hailstones is therefore the point $\vec a$ where:
56+
57+
$$
58+
\vec a = \vec v_1t_1 + \vec p_1 = \vec v_2t_2 = \vec p_2
59+
$$
60+
61+
This can be re-written as the system of equations:
62+
63+
$$
64+
\begin{bmatrix}\vec v_1 &-\vec v_2\end{bmatrix}\begin{bmatrix}t_1\\t_2\end{bmatrix} = \vec p_2 - \vec p_1
65+
$$
66+
67+
Solving this system of equations for each pair of hailstones will give us the values of $t_1$ and $t_2$ that can then be used to compute the coordinates of their intersection, $\vec a$.
68+
69+
```{r}
70+
71+
# Combine all hailstones' paths pairwise and solve the system of equations
72+
pairs <- inner_join(
73+
vecs_2d,
74+
vecs_2d,
75+
join_by(x$id < y$id),
76+
suffix = c("1", "2")
77+
) |>
78+
mutate(
79+
A = map2(v1, v2, ~ cbind(..1, -..2)),
80+
b = map2(p1, p2, ~ ..2 - ..1),
81+
det = map_dbl(A, det),
82+
t = pmap(lst(A, b, det), \(A, b, det) if (det != 0) as.vector(solve(A, b)))
83+
) |>
84+
unnest_wider(t, names_sep = "") |>
85+
86+
# Check if each path cross is within the bounding box and forward in time
87+
mutate(
88+
intersection = pmap(lst(t1, v1, p1), ~ ..1 * ..2 + ..3),
89+
in_bounds = map_lgl(intersection, ~ all(between(.x, bound_min, bound_max))),
90+
future_time = t1 >= 0 & t2 >= 0,
91+
flag = replace_na(in_bounds & future_time, FALSE)
92+
)
93+
94+
# Count the number of future-crossing paths:
95+
pairs |>
96+
pull(flag) |>
97+
sum()
98+
99+
```
100+
101+
## Part 2
102+
103+
Now our equation has changed. For each hailstone $i$, and for our initial position $\vec p_*$ and velocity $\vec v_*$, we have the following relationship, where $t_i$ is the nonzero collision time of our rock and the given hailstone:
104+
105+
$$
106+
(\vec v_* - \vec v_i)t_i = \vec p_* - \vec p_i
107+
$$
108+
109+
Since $t_i$ is a scalar for each $i$, then $\vec v_i - \vec v_*$ and $\vec p_i - \vec p_*$ are scalar multiples of each other. Thanks to a hint from Reddit user [u/evouga](https://www.reddit.com/r/adventofcode/comments/18pnycy/comment/kepu26z/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button), as these vectors are parallel, their cross product is zero, meaning that for all $i$:
110+
111+
$$
112+
(\vec p_* - \vec p_i) \times (\vec v_* - \vec v_i) = 0
113+
$$
114+
115+
Expanding this equation by the distributive property of the vector cross product, we get:
116+
117+
$$
118+
(\vec p_* \times \vec v_*) - (\vec p_* \times \vec v_i) - (\vec p_i \times \vec v_*) + (\vec p_i \times \vec v_i) = 0
119+
$$
120+
121+
Via [properties of the cross product](https://en.wikipedia.org/wiki/Cross_product#Conversion_to_matrix_multiplication), we can then represent this as:
122+
123+
$$
124+
(\vec p_* \times \vec v_*) - [\vec v_i]_\times^\intercal \vec p_* - [\vec p_i]_\times \vec v_* + (\vec p_i \times \vec v_i) = 0
125+
$$
126+
127+
where $[\vec a]_\times$ is defined as:
128+
129+
$$
130+
[\vec a]_\times = \begin{bmatrix}0 & -a_3 & a_2 \\ a_3 & 0 & -a_1 \\ -a_2 & a_1 & 0\end{bmatrix}
131+
$$
132+
133+
We can now (nearly) re-write this as a system of linear equations:
134+
135+
$$
136+
A_i\vec x = \vec b_i + (\vec p_* \times \vec v_*)
137+
$$
138+
139+
where
140+
141+
$$
142+
A_i = \begin{bmatrix}[\vec v_i]_\times^\intercal & [\vec p_i]_\times\end{bmatrix}, \quad \vec x = \begin{bmatrix}\vec p_* \\ \vec v_*\end{bmatrix}, \quad b_i = (\vec p_i \times \vec v_i)
143+
$$
144+
145+
Since this equation holds for all $i$, we can remove the needless term $(\vec p_* \times \vec v_*)$ and solve for $\vec x$ by subtracting two of these linear systems of equations from each other (using $i = 1,2$ as below, or any other two values of $i$ whose vectors from part 1 are not parallel):
146+
147+
$$
148+
(A_1 - A_2)\vec x = \vec b_1 - \vec b_2
149+
$$
150+
151+
Finally, since we've arrived at a system of 3 equations and 6 unknowns, we append $A$ and $\vec b$ with an additional pair of equations (using $i = 2,3$, for example) to solve for a final unique result:
152+
153+
$$
154+
\begin{bmatrix}A_1 - A_2\\A_2 - A_3\end{bmatrix}\vec x = \begin{bmatrix}\vec b_1 - \vec b_2\\ \vec b_2 - \vec b_3\end{bmatrix}
155+
$$
156+
157+
```{r}
158+
159+
# Define a function to compute the skeq symmetric matrix [a]_x
160+
skewsym <- function(x) {
161+
matrix(c(0, x[[3]], -x[[2]], -x[[3]], 0, x[[1]], x[[2]], -x[[1]], 0), ncol = 3)
162+
}
163+
164+
# For the first three vectors in our list, compute their A and b values
165+
lineqs <- vecs_3d |>
166+
slice_head(n = 3) |>
167+
mutate(
168+
A = map2(p, v, \(p, v) cbind(t(skewsym(v)), skewsym(p))),
169+
b = map2(p, v, \(p, v) pracma::cross(p, v))
170+
)
171+
172+
# Combine the 3 linear equations into a single system & solve
173+
A <- rbind(lineqs$A[[1]] - lineqs$A[[2]], lineqs$A[[2]] - lineqs$A[[3]])
174+
b <- rbind(lineqs$b[[1]] - lineqs$b[[2]], lineqs$b[[2]] - lineqs$b[[3]])
175+
x <- solve(A, b)
176+
```
177+
178+
Finally, add together the three px, py, and pz coordinates for the initial position:
179+
180+
```{r}
181+
sum(x[1:3]) |>
182+
format(scientific = FALSE)
183+
```

0 commit comments

Comments
 (0)