|
| 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