Skip to main content

aoc/year2023/
day24.rs

1//! # Never Tell Me The Odds
2//!
3//! ## Part One
4//!
5//! We find the intersection for each pair of hailstones by solving a pair of linear simultaneous
6//! equations in 2 unknowns:
7//!
8//! * `a` and `g` are the x positions of the pair of hailstones.
9//! * `b` and `h` are the y positions.
10//! * `d` and `j` are the x velocities.
11//! * `e` and `k` are the y velocities.
12//! * Let `t` and `u` be the times that the first and second hailstone respectively are at the
13//!   intersection point.
14//!
15//! Then we can write:
16//!
17//! * `a + dt = g + ju` => `dt - ju = g - a`
18//! * `b + et = h + ku` => `et - ku = h - b`
19//!
20//! In matrix form:
21//!
22//! ```none
23//!     | d  -j ||u| = | g - a |
24//!     | e  -k ||t|   | h - b |
25//! ```
26//!
27//! Solve by finding the inverse of the 2x2 matrix and premultiplying both sides. The inverse is:
28//!
29//! ```none
30//!    ______1______ | -k  j |
31//!    d(-k) - (-j)e | -e  d |
32//! ```
33//!
34//! Then we check that both times are non-negative and that the intersection point is inside the
35//! target area.
36//!
37//! ## Part Two
38//!
39//! First we choose 3 arbitrary hailstones. Then we subtract the position and velocity of
40//! the first to make the other two relative.
41//!
42//! The two hailstones will intercept a line leaving the origin. We can determine this line
43//! by intersecting the two planes that the hailstones' velocities lie in. These planes are
44//! defined by a normal vector orthogonal to the plane.
45//!
46//! This normal vector is the [cross product](https://en.wikipedia.org/wiki/Cross_product) of
47//! any two vectors that lie in the plane, in this case the velocity and also the vector from the
48//! origin to the starting location of the hailstone.
49//!
50//! The direction but not necessarily the magnitude of the velocity is then given by the cross
51//! product of the two normals.
52//!
53//! Given the rock direction we can calculate the times that the two hailstones are intercepted
54//! then use this to determine the original position of the rock, as long as the two times
55//! are different.
56use crate::util::iter::*;
57use crate::util::math::*;
58use crate::util::parse::*;
59use std::ops::{Add, RangeInclusive, Sub};
60
61const RANGE: RangeInclusive<i64> = 200_000_000_000_000..=400_000_000_000_000;
62
63#[derive(Clone, Copy)]
64struct Vector {
65    x: i128,
66    y: i128,
67    z: i128,
68}
69
70impl Add for Vector {
71    type Output = Self;
72
73    fn add(self, rhs: Self) -> Self {
74        Vector { x: self.x + rhs.x, y: self.y + rhs.y, z: self.z + rhs.z }
75    }
76}
77
78impl Sub for Vector {
79    type Output = Self;
80
81    fn sub(self, rhs: Self) -> Self {
82        Vector { x: self.x - rhs.x, y: self.y - rhs.y, z: self.z - rhs.z }
83    }
84}
85
86impl Vector {
87    fn cross(self, other: Self) -> Self {
88        let x = self.y * other.z - self.z * other.y;
89        let y = self.z * other.x - self.x * other.z;
90        let z = self.x * other.y - self.y * other.x;
91        Vector { x, y, z }
92    }
93
94    // Changes the magnitude (but not direction) of the vector.
95    // Prevents numeric overflow.
96    fn gcd(self) -> Self {
97        let gcd = self.x.gcd(self.y).gcd(self.z);
98        Vector { x: self.x / gcd, y: self.y / gcd, z: self.z / gcd }
99    }
100
101    fn sum(self) -> i128 {
102        self.x + self.y + self.z
103    }
104}
105
106pub fn parse(input: &str) -> Vec<[i64; 6]> {
107    input.iter_signed().chunk::<6>().collect()
108}
109
110pub fn part1(input: &[[i64; 6]]) -> u32 {
111    let mut result = 0;
112
113    for (index, &[a, b, _, c, d, _]) in input[1..].iter().enumerate() {
114        for &[e, f, _, g, h, _] in &input[..index + 1] {
115            // If the determinant is zero there is no solution possible
116            // which implies the trajectories are parallel.
117            let determinant = d * g - c * h;
118            if determinant == 0 {
119                continue;
120            }
121
122            // Invert the 2x2 matrix then multiply by the respective columns to find the times.
123            let t = (g * (f - b) - h * (e - a)) / determinant;
124            let u = (c * (f - b) - d * (e - a)) / determinant;
125
126            // We can pick either the first or second hailstone to find the intersection position.
127            let x = a + t * c;
128            let y = b + t * d;
129
130            // Both times must be in the future and the position within the specified area.
131            if t >= 0 && u >= 0 && RANGE.contains(&x) && RANGE.contains(&y) {
132                result += 1;
133            }
134        }
135    }
136
137    result
138}
139
140pub fn part2(input: &[[i64; 6]]) -> i128 {
141    // Calculations need the range of `i128`.
142    let widen = |i: usize| {
143        let [px, py, pz, vx, vy, vz] = input[i].map(|n| n as i128);
144        let p = Vector { x: px, y: py, z: pz };
145        let v = Vector { x: vx, y: vy, z: vz };
146        (p, v)
147    };
148
149    // Take 3 arbitrary hailstones.
150    let (p0, v0) = widen(0);
151    let (p1, v1) = widen(1);
152    let (p2, v2) = widen(2);
153
154    // Subtract the positions and velocities to make them relative.
155    // The first hailstone is stationary at the origin.
156    let p3 = p1 - p0;
157    let p4 = p2 - p0;
158    let v3 = v1 - v0;
159    let v4 = v2 - v0;
160
161    // Find the normal to the plane that the second and third hailstones velocity lies in.
162    // This is the cross product of their respective position and velocity.
163    // The cross product `s` of these two vectors is the same direction but not necessarily the
164    // same magnitude of the desired velocity of the rock.
165    // Only the direction is relevant (not the magnitude) so we can normalize the vector by the
166    // GCD of its components in order to prevent numeric overflow.
167    let q = v3.cross(p3).gcd();
168    let r = v4.cross(p4).gcd();
169    let s = q.cross(r).gcd();
170
171    // Find the times when the second and third hailstone intercept this vector.
172    // If the times are different then we can extrapolate the original position of the rock.
173    let t = (p3.y * s.x - p3.x * s.y) / (v3.x * s.y - v3.y * s.x);
174    let u = (p4.y * s.x - p4.x * s.y) / (v4.x * s.y - v4.y * s.x);
175    assert!(t != u);
176
177    // Calculate the original position of the rock, remembering to add the first hailstone's
178    // position to convert back to absolute coordinates.
179    let a = (p0 + p3).sum();
180    let b = (p0 + p4).sum();
181    let c = (v3 - v4).sum();
182    (u * a - t * b + u * t * c) / (u - t)
183}