Advent of Code 2023 - Day 24Never Tell Me The Odds

R | Problem statement | Source code | Tags: GeometryMathematics

Part 1

Each hailstone is a parametric equation. If we are given x1, y1, z1 @ vx1, vy1, vz1, then the position of the hailstone at time t is given by:

p1(t)=p1(0)+tv1\mathbf{p}_1(t) = \mathbf{p}_1(0) + t \mathbf{v}_1

where p1(0)=(x1,y1,z1)\mathbf{p}_1(0) = (x_1, y_1, z_1)^\top and v1=(vx1,vy1,vz1)\mathbf{v}_1 = (vx_1, vy_1, vz_1)^\top.

In part 1 we only care about 2D, so instead p1(0)=(x1,y1)\mathbf{p}_1(0) = (x_1, y_1)^\top and v1=(vx1,vy1)\mathbf{v}_1 = (vx_1, vy_1)^\top. To find the intersection of two hailstone paths (perhaps at different times) is to find the solution to:

p1(t1)=p2(t2)p1(0)+t1v1=p2(0)+t2v2[vx1vx2vy1vy2][t1t2]=[x2x1y2y1]\begin{aligned} \mathbf{p}_1(t_1) &= \mathbf{p}_2(t_2)\\ \mathbf{p}_1(0) + t_1 \mathbf{v}_1 &= \mathbf{p}_2(0) + t_2 \mathbf{v}_2\\ \begin{bmatrix}vx_1 & -vx_2 \\ vy_1 & -vy_2\end{bmatrix} \begin{bmatrix}t_1 \\ t_2\end{bmatrix} &= \begin{bmatrix}x_2 - x_1 \\ y_2 - y_1\end{bmatrix} \end{aligned}

This linear system can be directly solved in R gmp with solve.bigz. The system may not have a solution if the two paths are parallel; we additionally require t1,t20t_1, t_2 \geq 0 for the solution to be valid. The intersection point can be found by plugging t1t_1 into p1(t)\mathbf{p}_1(t).

intersection2d <- function(l1, l2) {
v_mat <- matrix(c(l1[4], l1[5], -l2[4], -l2[5]), nrow = 2)
b_vec <- c(l2[1] - l1[1], l2[2] - l1[2])
sol <- tryCatch(solve.bigz(v_mat, b_vec), error = function(e) NULL)
if (is.null(sol) || any(sol < 0)) {
return(NULL)
}
c(l1[1], l1[2]) + sol[1] * c(l1[4], l1[5])
}
R

After obtaining the intersection point, we can easily test if it's in the test area.

Part 2

Now coordinates are 3D. We need some p(0)\mathbf{p}(0) and v\mathbf{v}, such that for every ii,

pi(0)+tivi=p(0)+tivpi(0)+ti(viv)=p(0)\begin{aligned} \mathbf{p}_i(0) + t_i \mathbf{v}_i = \mathbf{p}(0) + t_i \mathbf{v}\\ \mathbf{p}_i(0) + t_i (\mathbf{v}_i - \mathbf{v}) = \mathbf{p}(0) \end{aligned}

If we take two trails, ii and jj, we can eliminate p(0)\mathbf{p}(0):

pi(0)+ti(viv)=pj(0)+tj(vjv)ti(viv)tj(vjv)=pj(0)pi(0)\begin{aligned} \mathbf{p}_i(0) + t_i (\mathbf{v}_i - \mathbf{v}) &= \mathbf{p}_j(0) + t_j (\mathbf{v}_j - \mathbf{v})\\ t_i (\mathbf{v}_i - \mathbf{v}) - t_j (\mathbf{v}_j - \mathbf{v}) &= \mathbf{p}_j(0) - \mathbf{p}_i(0) \end{aligned}

This means that pj(0)pi(0)\mathbf{p}_j(0) - \mathbf{p}_i(0) can be expressed as a linear combination of viv\mathbf{v}_i - \mathbf{v} and vjv\mathbf{v}_j - \mathbf{v}, i.e., pj(0)pi(0)\mathbf{p}_j(0) - \mathbf{p}_i(0) is in the plane spanned by {viv,vjv}\{\mathbf{v}_i - \mathbf{v}, \mathbf{v}_j - \mathbf{v}\}. Note that vjvi\mathbf{v}_j - \mathbf{v}_i is also in this plane, so we can express this in terms of the scalar triple product:

((pj(0)pi(0))×(vjvi))(viv)=0((\mathbf{p}_j(0) - \mathbf{p}_i(0))\times (\mathbf{v}_j - \mathbf{v}_i)) \cdot (\mathbf{v}_i - \mathbf{v}) = 0

(because ×\times gives a normal vector to the plane, and \cdot returns 0 if the two vectors are perpendicular).

Now this can be separated into an equation in v\mathbf{v}:

((pj(0)pi(0))×(vjvi))v=((pj(0)pi(0))×(vjvi))vi((\mathbf{p}_j(0) - \mathbf{p}_i(0))\times (\mathbf{v}_j - \mathbf{v}_i)) \cdot \mathbf{v} = ((\mathbf{p}_j(0) - \mathbf{p}_i(0))\times (\mathbf{v}_j - \mathbf{v}_i)) \cdot \mathbf{v}_i

This is a single linear equation. Because v\mathbf{v} has three components, we need at least three equations in this form, which means taking three trails.

[(p2(0)p1(0))×(v2v1)(p3(0)p2(0))×(v3v2)(p1(0)p3(0))×(v1v3)]v=[((p2(0)p1(0))×(v2v1))v1((p3(0)p2(0))×(v3v2))v2((p1(0)p3(0))×(v1v3))v3]\begin{bmatrix} (\mathbf{p}_2(0) - \mathbf{p}_1(0))\times (\mathbf{v}_2 - \mathbf{v}_1)\\ (\mathbf{p}_3(0) - \mathbf{p}_2(0))\times (\mathbf{v}_3 - \mathbf{v}_2)\\ (\mathbf{p}_1(0) - \mathbf{p}_3(0))\times (\mathbf{v}_1 - \mathbf{v}_3) \end{bmatrix} \mathbf{v} = \begin{bmatrix} ((\mathbf{p}_2(0) - \mathbf{p}_1(0))\times (\mathbf{v}_2 - \mathbf{v}_1)) \cdot \mathbf{v}_1\\ ((\mathbf{p}_3(0) - \mathbf{p}_2(0))\times (\mathbf{v}_3 - \mathbf{v}_2)) \cdot \mathbf{v}_2\\ ((\mathbf{p}_1(0) - \mathbf{p}_3(0))\times (\mathbf{v}_1 - \mathbf{v}_3)) \cdot \mathbf{v}_3 \end{bmatrix}
num_eq <- 3
A <- matrix(as.bigz(0), nrow = num_eq, ncol = 3)
b <- matrix(as.bigz(0), nrow = num_eq, ncol = 1)
for (i in 1:num_eq) {
j <- i %% num_eq + 1
v_diff <- hails[[j]][4:6] - hails[[i]][4:6]
p_diff <- hails[[j]][1:3] - hails[[i]][1:3]
cross_prod <- cross(v_diff, p_diff)
A[i, ] <- cross_prod
b[i] <- sum(cross_prod * hails[[i]][4:6])
}
v <- tryCatch(solve.bigz(A, b), error = function(e) NULL)
R

I didn't really verify that this solution works on the remaining hail paths, because they'd better be (otherwise there's no answer). After solving for v\mathbf{v}, we can plug it into two lines to solve for tt on 2D (it's guaranteed that the z-coordinate must match as well, otherwise it wouldn't be a solution). This can take advantage of the existing intersection2d function.

p(0)=p1(0)+t1(v1v)=p2(0)+t2(v2v)\mathbf{p}(0) = \mathbf{p}_1(0) + t_1 (\mathbf{v}_1 - \mathbf{v}) = \mathbf{p}_2(0) + t_2 (\mathbf{v}_2 - \mathbf{v})
p1 <- as.bigz(hails[[1]][1:3])
p2 <- as.bigz(hails[[2]][1:3])
v1 <- as.bigz(hails[[1]][4:6])
v2 <- as.bigz(hails[[2]][4:6])
pxy <- intersection2d(c(p1, v1 - v), c(p2, v2 - v))
R

Finally, we can deduce the z-coordinate.

t1 <- (pxy[1] - p1[1]) / (v1[1] - v[1])
pz <- p1[3] + t1 * (v1[3] - v[3])
R