adventofcode2023/day24/src/main.rs

193 lines
8.6 KiB
Rust
Raw Normal View History

2023-12-25 19:13:31 -06:00
use num::ToPrimitive;
use std::fs::read_to_string;
use std::time::Instant;
fn get_numbers_in_line_split_iter<T: std::str::FromStr>(str: &str) -> impl Iterator<Item=T> + '_ {
str.split([' ', ',']).filter_map(|substr| substr.parse::<T>().ok())
}
struct Hailstone {
pos: (i64, i64, i64),
vel: (i64, i64, i64),
}
enum LineIntersection<T: PartialOrd> {
// Has position of intersection
Intersects((T, T)),
// Has coefficient of the line (a, b, c), with line being ax + bx + c = 0
Colinear((T, T, T)),
None,
}
// lXpY = line X point Y
fn xy_intersection<T>(l1p1: (T, T), l1p2: (T, T), l2p1: (T, T), l2p2: (T, T)) -> LineIntersection<T>
where T: PartialOrd + std::ops::Neg<Output=T> + std::ops::Sub<Output=T> + num::Zero,
for<'a> &'a T: std::ops::Sub<Output=T> + std::ops::Mul<Output=T> + std::ops::Div<Output=T>
{
let l1xdiff = &l1p1.0 - &l1p2.0;
let l1ydiff = &l1p1.1 - &l1p2.1;
let l2xdiff = &l2p1.0 - &l2p2.0;
let l2ydiff = &l2p1.1 - &l2p2.1;
let denom = &l1xdiff * &l2ydiff - &l1ydiff * &l2xdiff;
if denom == T::zero() {
let proj1 = &l1p1.0 * &l1ydiff - &l1p1.1 * &l1xdiff;
let proj2 = &l2p1.0 * &l1ydiff - &l2p1.1 * &l1xdiff;
if &proj1 - &proj2 == T::zero() {
return LineIntersection::Colinear((l1xdiff, -l1ydiff, -proj1));
} else {
return LineIntersection::None;
}
}
let l1det = (&l1p1.0 * &l1p2.1) - (&l1p1.1 * &l1p2.0);
let l2det = (&l2p1.0 * &l2p2.1) - (&l2p1.1 * &l2p2.0);
let detx = &l1det * &l2xdiff - &l2det * &l1xdiff;
let dety = &l1det * &l2ydiff - &l2det * &l1ydiff;
LineIntersection::Intersects((&detx / &denom, &dety / &denom))
}
fn vec_add<T>(v1: (T, T, T), v2: (T, T, T)) -> (T, T, T)
where T: std::ops::Add<Output=T> {
(v1.0 + v2.0, v1.1 + v2.1, v1.2 + v2.2)
}
fn cross_prod<T>(v1: (T, T, T), v2: (T, T, T)) -> (T, T, T)
where T: std::ops::Sub<T, Output=T>, for<'a> &'a T: std::ops::Mul<&'a T, Output=T> {
(&v1.1 * &v2.2 - &v1.2 * &v2.1, &v1.2 * &v2.0 - &v1.0 * &v2.2, &v1.0 * &v2.1 - &v1.1 * &v2.0)
}
// Get integer intersecting line at the right timeframes: (x, y, z), (vx, vy, vz)
fn get_intersecting_line(hailstones: &[Hailstone]) -> ((i64, i64, i64), (i64, i64, i64)) {
// Moving the frame of reference to hailstones[0] so we can compute the rest around origin
// With one line being reduced to start (0, 0, 0) and velocity (0, 0, 0) it'll be simpler
let map_pos = hailstones[0].pos;
let map_vel = hailstones[0].vel;
let hails = hailstones[1..=2].iter().map(
|hail| {
let pos = ((hail.pos.0 - map_pos.0) as i128, (hail.pos.1 - map_pos.1) as i128, (hail.pos.2 - map_pos.2) as i128);
let vel = ((hail.vel.0 - map_vel.0) as i128, (hail.vel.1 - map_vel.1) as i128, (hail.vel.2 - map_vel.2) as i128);
(pos, vel)
}
).collect::<Vec<_>>();
// cross product of vectors from origin to two points of the first other line
let plane1 = cross_prod(hails[0].0, vec_add(hails[0].0, hails[0].1));
// cross product of vectors from origin to two points of the second other line
let plane2 = cross_prod(hails[1].0, vec_add(hails[1].0, hails[1].1));
// cross product of thw two planes to reduce the space to a single line
let line = cross_prod(plane1, plane2);
// The line now represents a good direction with potentially wrong length (not a problem),
// and potentially wrong sign (a problem)
// find the smallest possible vel using gcd
let gcd = [line.0, line.1, line.2].iter()
.fold(0i128, |acc, &elem| num::integer::gcd(acc, elem));
let mut vel = (line.0 / gcd, line.1 / gcd, line.2 / gcd);
// We need to find the start point
// We know of one line at (0, 0, 0) due to mapping, we'll use it with another line to figure
// out the exact starting position (mapped)
// start + t0 * vel = 0 - the line at 0
// start + t1 * vel = p1 + t1*vel1 - the other line
// becomes
// start = p1 + t1 * (vel1 - vel)
// start = -t0 * vel
// get rid of start
// t0 * vel + t1 * (vel1 - vel) = -p1 - the vectors are 3 dimensional and we have 2 variables
// solve system of just taking the x and y from vectors
let mut det = vel.0 * (hails[1].1.1 - vel.1) - vel.1 * (hails[1].1.0 - vel.0);
let mut t0 = -hails[1].0.0 * (hails[1].1.1 - vel.1) / det + hails[1].0.1 * (hails[1].1.0 - vel.0) / det;
let mut start = (-t0 * vel.0, -t0 * vel.1, -t0 * vel.2);
// check on other 2 hailstones if the answer is consistent, otherwise flip sign
let mut wrong = false;
let rock_pos = [start.0, start.1, start.2];
let rock_vel = [vel.0, vel.1, vel.2];
'outer: for i in 0..2 {
let mut last_solution: Option<i128> = None;
let hail_pos = [hails[i].0.0, hails[i].0.1, hails[i].0.2];
let hail_vel = [hails[i].1.0, hails[i].1.1, hails[i].1.2];
for j in 0..3 {
let denom = rock_vel[j] - hail_vel[j];
if denom != 0 {
let new_solution = (hail_pos[j] - rock_pos[j]) / denom;
if new_solution < 0 {
wrong = true;
break 'outer;
}
match last_solution {
None => last_solution = Some(new_solution),
Some(s) => {
if s != new_solution {
wrong = true;
break 'outer;
}
}
}
}
}
}
// flip the sign
if wrong {
vel = (-vel.0, -vel.1, -vel.2);
det = vel.0 * (hails[1].1.1 - vel.1) - vel.1 * (hails[1].1.0 - vel.0);
t0 = -hails[1].0.0 * (hails[1].1.1 - vel.1) / det + hails[1].0.1 * (hails[1].1.0 - vel.0) / det;
start = (-t0 * vel.0, -t0 * vel.1, -t0 * vel.2);
}
let start_unmapped = (start.0 as i64 + map_pos.0, start.1 as i64 + map_pos.1, start.2 as i64 + map_pos.2);
let vel_unmapped = (vel.0 as i64 + map_vel.0, vel.1 as i64 + map_vel.1, vel.2 as i64 + map_vel.2);
(start_unmapped, vel_unmapped)
}
fn main() {
let time_start = Instant::now();
let input_str = read_to_string("input.txt").unwrap();
let time_start_no_io = Instant::now();
// Shift the interval to be centered around 0, avoids numerical errors
const COORD_SHIFT: i64 = -3e14 as i64;
const MIN_POS: f64 = -1e14;
const MAX_POS: f64 = 1e14;
let hailstones = input_str.lines().map(|line| {
let mut it = get_numbers_in_line_split_iter::<i64>(line);
Hailstone {
pos: (it.next().unwrap() + COORD_SHIFT, it.next().unwrap() + COORD_SHIFT, it.next().unwrap() + COORD_SHIFT),
vel: (it.next().unwrap(), it.next().unwrap(), it.next().unwrap()),
}
}).collect::<Vec<_>>();
// Part 1
let mut count1 = 0;
for (i, h1) in hailstones.iter().enumerate() {
for h2 in hailstones[i + 1..].iter() {
let l1p1 = (h1.pos.0 as f64, h1.pos.1 as f64);
let l1p2 = ((h1.pos.0 + h1.vel.0) as f64, (h1.pos.1 + h1.vel.1) as f64);
let l2p1 = (h2.pos.0 as f64, h2.pos.1 as f64);
let l2p2 = ((h2.pos.0 + h2.vel.0) as f64, (h2.pos.1 + h2.vel.1) as f64);
match xy_intersection(l1p1, l1p2, l2p1, l2p2) {
LineIntersection::Intersects((xr, yr)) => {
let (x, y) = (xr.to_f64().unwrap(), yr.to_f64().unwrap());
// Check if it's not in the past, thankfully velocity components are never 0
if (x - h1.pos.0 as f64) / h1.vel.0 as f64 >= 0.
&& (x - h2.pos.0 as f64) / h2.vel.0 as f64 >= 0. {
if x >= MIN_POS && x <= MAX_POS && y >= MIN_POS && y <= MAX_POS {
count1 += 1;
}
}
}
LineIntersection::Colinear((_, _, _)) => {
// Seems this case isn't used
// otherwise I'd have to check if they go in the same direction
}
LineIntersection::None => {
}
}
}
}
// Part 2
let (start, vel) = get_intersecting_line(&hailstones);
let corrected_start = (start.0 - COORD_SHIFT, start.1 - COORD_SHIFT, start.2 - COORD_SHIFT);
let summed_coords = corrected_start.0 + corrected_start.1 + corrected_start.2;
let elapsed = time_start.elapsed().as_micros();
let elapsed_no_io = time_start_no_io.elapsed().as_micros();
println!("Time: {}us", elapsed);
println!("Time without file i/o: {}us", elapsed_no_io);
println!("Count1: {}", count1);
println!("Start: {:?}, vel: {:?}", corrected_start, vel);
println!("Summed start coords: {:?}", summed_coords);
}