From dfd8b2b985575cebf537f3ba9062a5471ab1f61c Mon Sep 17 00:00:00 2001 From: Bert Peters Date: Fri, 17 Dec 2021 10:04:36 +0100 Subject: [PATCH] Partially smart, partially brute-force day 17 --- 2021/inputs/17.txt | 1 + 2021/src/day17.rs | 178 +++++++++++++++++++++++++++++++++++++++- 2021/src/samples/17.txt | 1 + 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 2021/inputs/17.txt create mode 100644 2021/src/samples/17.txt diff --git a/2021/inputs/17.txt b/2021/inputs/17.txt new file mode 100644 index 0000000..f4deca8 --- /dev/null +++ b/2021/inputs/17.txt @@ -0,0 +1 @@ +target area: x=14..50, y=-267..-225 diff --git a/2021/src/day17.rs b/2021/src/day17.rs index 113ba49..3df1db1 100644 --- a/2021/src/day17.rs +++ b/2021/src/day17.rs @@ -1,9 +1,179 @@ use std::io::Read; +use std::ops::RangeInclusive; -pub fn part1(_input: &mut dyn Read) -> String { - todo!() +use itertools::Itertools; +use itertools::MinMaxResult; +use nom::bytes::complete::tag; +use nom::combinator::map; +use nom::sequence::preceded; +use nom::sequence::separated_pair; +use nom::Finish; +use nom::IResult; + +#[inline] +fn solve_quadratic(a: f64, b: f64, c: f64) -> Option { + let d = b * b - 4. * a * c; + + if d < 0. { + None + } else { + // Don't care about the smaller solution due to problem statement + Some((-b - d.sqrt()) / 2. / a) + } } -pub fn part2(_input: &mut dyn Read) -> String { - todo!() +fn position(initial: i32, time: i32) -> i32 { + time * (2 * initial - time + 1) / 2 +} + +fn find_hit(initial: i32, range: &RangeInclusive) -> Option> { + // y position at time x: f(x) = x * (1 + initial + initial - x) / 2 + // = -1/2x^2 + (initial + 0.5)x + // + // to hit, find x := (max(box) + min(box)) / 2 = f(x) + // = -1/2x^2 + (initial + 0.5)x + // -1/2x^2 + (initial + 0.5)x - (max(box) + min(box)) / 2 = 0 + let middle = (*range.start() + *range.end()) as f64 / 2.; + let b = initial as f64 + 0.5; + let hit = solve_quadratic(-0.5, b, -middle)? as i32; + + if hit < 0 { + // Should not happen because of the shape but for correctness + None + } else { + let min_hit = (0..=hit) + .rev() + .take_while(|&n| range.contains(&position(initial, n))) + .min(); + + let max_hit = ((hit + 1)..) + .take_while(|&n| range.contains(&position(initial, n))) + .max(); + + match (min_hit, max_hit) { + (Some(min), Some(max)) => Some(min..=max), + (Some(val), None) | (None, Some(val)) => Some(val..=val), + _ => None, + } + } +} + +fn find_speed(x: i32, range: &RangeInclusive) -> Option> { + let mut min = 0; + let mut max = *range.end(); + + // Need to tweak the formula as x slows down + let x_pos = |speed| position(speed, speed.min(x)); + + while max >= min { + let speed = (max + min) / 2; + + let pos = x_pos(speed); + + if range.contains(&x_pos(speed)) { + let min_speed = (0..speed) + .rev() + .take_while(|&speed| range.contains(&x_pos(speed))) + .min() + .unwrap_or(speed); + + let max_speed = ((speed + 1)..) + .take_while(|&speed| range.contains(&x_pos(speed))) + .max() + .unwrap_or(speed); + return Some(min_speed..=max_speed); + } else if pos < *range.start() { + min = speed + 1; + } else { + max = speed - 1; + } + } + + None +} + +fn parse_range(input: &[u8]) -> IResult<&[u8], RangeInclusive> { + use nom::character::complete::i32; + + map(separated_pair(i32, tag(".."), i32), |(start, end)| { + start..=end + })(input) +} + +fn parse_input(input: &[u8]) -> IResult<&[u8], (RangeInclusive, RangeInclusive)> { + preceded( + tag("target area: x="), + separated_pair(parse_range, tag(", y="), parse_range), + )(input) +} + +pub fn part1(input: &mut dyn Read) -> String { + let mut buffer = Vec::new(); + input.read_to_end(&mut buffer).unwrap(); + + let (x_range, y_range) = parse_input(&buffer).finish().unwrap().1; + + let check_value = |y_speed| { + let mut time = find_hit(y_speed, &y_range)?; + + if time.any(|time| find_speed(time, &x_range).is_some()) { + Some(position(y_speed, y_speed)) + } else { + None + } + }; + + (0..1000).filter_map(check_value).max().unwrap().to_string() +} + +pub fn part2(input: &mut dyn Read) -> String { + let mut buffer = Vec::new(); + input.read_to_end(&mut buffer).unwrap(); + + let (x_range, y_range) = parse_input(&buffer).finish().unwrap().1; + + let num_options = |y_speed| { + let time = find_hit(y_speed, &y_range)?; + let range = time + .filter_map(|time| find_speed(time, &x_range)) + .flat_map(|x| [*x.start(), *x.end()]) + .minmax(); + + Some(match range { + MinMaxResult::NoElements => 0, + MinMaxResult::OneElement(_) => 1, + MinMaxResult::MinMax(min, max) => max - min + 1, + }) + }; + + (-1000..1000) + .filter_map(num_options) + .sum::() + .to_string() +} + +#[cfg(test)] +mod tests { + use crate::test_implementation; + + use super::*; + + const SAMPLE: &[u8] = &*b"target area: x=20..30, y=-10..-5"; + + #[test] + fn test_find_hit() { + assert_eq!(find_hit(2, &(-10..=-5)), Some(7..=7)); + assert_eq!(find_hit(3, &(-10..=-5)), Some(9..=9)); + assert_eq!(find_hit(0, &(-10..=-5)), Some(4..=5)); + } + + #[test] + fn sample_part1() { + test_implementation(part1, SAMPLE, 45); + } + + #[test] + fn sample_part2() { + test_implementation(part2, SAMPLE, 112); + } } diff --git a/2021/src/samples/17.txt b/2021/src/samples/17.txt new file mode 100644 index 0000000..a07e02d --- /dev/null +++ b/2021/src/samples/17.txt @@ -0,0 +1 @@ +target area: x=20..30, y=-10..-5