diff --git a/aoc2021/input/day17.txt b/aoc2021/input/day17.txt new file mode 100644 index 0000000..ba5668c --- /dev/null +++ b/aoc2021/input/day17.txt @@ -0,0 +1 @@ +target area: x=277..318, y=-92..-53 diff --git a/aoc2021/input/day17_provided.txt b/aoc2021/input/day17_provided.txt new file mode 100644 index 0000000..a07e02d --- /dev/null +++ b/aoc2021/input/day17_provided.txt @@ -0,0 +1 @@ +target area: x=20..30, y=-10..-5 diff --git a/aoc2021/src/day17.rs b/aoc2021/src/day17.rs new file mode 100644 index 0000000..b3734b6 --- /dev/null +++ b/aoc2021/src/day17.rs @@ -0,0 +1,159 @@ +use std::cmp::Ordering; +use std::fmt::Write; +use std::ops::RangeInclusive; + +use anyhow::{Context, Result}; + +const INPUT: &str = include_str!("../input/day17.txt"); + +pub fn run() -> Result { + let mut res = String::with_capacity(128); + + writeln!(res, "part 1: {}", part1(INPUT)?)?; + + Ok(res) +} + +fn part1(input: &str) -> Result { + let area: TargetArea = input + .parse() + .context("couldn't parse input to target area")?; + + let min_x_vel = if area.min_x() > 0 { + (0..).find(|x| ((x * (x + 1)) / 2) >= area.min_x()).unwrap() + } else if area.max_x() < 0 { + -(0..) + .find(|x| ((x * (x + 1)) / 2) >= area.max_x().abs()) + .unwrap() + } else { + 0 + }; + + let max_x_vel = if area.min_x() > 0 { + area.max_x() + } else if area.max_x() < 0 { + area.min_x() + } else { + 0 + }; + + // Rust ranges can only be increasing, so swap values around if negative + let (min_x_vel, max_x_vel) = (min_x_vel.min(max_x_vel), min_x_vel.max(max_x_vel)); + + // we could launch the prob downward, but in that case max Y reached would always be 0 + let min_y_vel = 0; + let max_y_vel = 5000; // idk + + (min_x_vel..=max_x_vel) + .flat_map(|x_vel| (min_y_vel..=max_y_vel).map(move |y_vel| (x_vel, y_vel))) + .filter_map(|(x_vel, y_vel)| throw(x_vel, y_vel, &area)) + .max() + .context("couldn't find any trajectory") +} + +fn throw(mut xvel: isize, mut yvel: isize, area: &TargetArea) -> Option { + let (mut pos_x, mut pos_y) = (0, 0); + let mut highest_y = 0; + + loop { + if area.contains(pos_x, pos_y) { + return Some(highest_y); + } + + // Three cases where we can stop here: + // - probe is lower than area, and gravity pulls it even lower + // - probe is on the left, and x velocity goes left + // - probe is on the right, and x velocity goes right + if (pos_y < area.min_y() && yvel <= 0) + || (xvel <= 0 && pos_x < area.min_x()) + || (xvel >= 0 && pos_x > area.max_x()) + { + // the probe will never reach the area, we can stop the simulation here + return None; + } + + // - The probe's x position increases by its x velocity. + pos_x += xvel; + // - The probe's y position increases by its y velocity. + pos_y += yvel; + // - Due to drag, the probe's x velocity changes by 1 toward the value 0; that is, it + // decreases by 1 if it is greater than 0, increases by 1 if it is less than 0, or does not + // change if it is already 0. + match xvel.cmp(&0) { + Ordering::Less => xvel += 1, + Ordering::Greater => xvel -= 1, + _ => {} + } + // - Due to gravity, the probe's y velocity decreases by 1. + yvel -= 1; + + // update highest seen y + highest_y = highest_y.max(pos_y); + } +} + +struct TargetArea { + x_range: RangeInclusive, + y_range: RangeInclusive, +} + +impl TargetArea { + fn contains(&self, x: isize, y: isize) -> bool { + self.x_range.contains(&x) && self.y_range.contains(&y) + } + + fn min_y(&self) -> isize { + *self.y_range.start() + } + + fn min_x(&self) -> isize { + *self.x_range.start() + } + + fn max_x(&self) -> isize { + *self.x_range.end() + } +} + +impl std::str::FromStr for TargetArea { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let s = s + .trim() + .strip_prefix("target area: ") + .context("missing target area while parsing")?; + + let (x, y) = s.split_once(", ").context("couldn't split on comma")?; + + let x = x.strip_prefix("x=").context("couldn't find `x=`")?; + let (min_x, max_x) = x.split_once("..").context("couldn't split on `..`")?; + let (min_x, max_x) = (min_x.parse()?, max_x.parse()?); + + let y = y.strip_prefix("y=").context("couldn't find `y=`")?; + let (min_y, max_y) = y.split_once("..").context("couldn't split on `..`")?; + let (min_y, max_y) = (min_y.parse()?, max_y.parse()?); + + Ok(Self { + x_range: min_x..=max_x, + y_range: min_y..=max_y, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const PROVIDED: &str = include_str!("../input/day17_provided.txt"); + + #[test] + fn part1_provided() { + assert_eq!(part1(PROVIDED).unwrap(), 45); + } + + #[test] + fn part1_real() { + assert_eq!(part1(INPUT).unwrap(), 4186); + } +} diff --git a/aoc2021/src/lib.rs b/aoc2021/src/lib.rs index f281e9a..cf569f2 100644 --- a/aoc2021/src/lib.rs +++ b/aoc2021/src/lib.rs @@ -16,3 +16,4 @@ pub mod day13; pub mod day14; pub mod day15; pub mod day16; +pub mod day17; diff --git a/aoc2021/src/main.rs b/aoc2021/src/main.rs index 524f73b..7dc5fe7 100644 --- a/aoc2021/src/main.rs +++ b/aoc2021/src/main.rs @@ -18,6 +18,7 @@ use aoc2021::day13; use aoc2021::day14; use aoc2021::day15; use aoc2021::day16; +use aoc2021::day17; fn main() -> Result<()> { let days: &[DayFunc] = &[ @@ -37,6 +38,7 @@ fn main() -> Result<()> { day14::run, day15::run, day16::run, + day17::run, ]; aoc::run(days)