1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
//! This is my solution for [Advent of Code - Day 6: _Wait For It_](https://adventofcode.com/2023/day/6)
//!
//! Today was solved with maths rather than brute force. The parsing is not much effort, but different for each part.
//! [`part_1_line_parser`] and [`part_2_line_parser`] contain these differences, and the relevant one is passed to
//! [`parse_input`] giving a list of [`Race`]s.
//!
//! [`find_count_of_winning_hold_times`] uses the quadratic formula to calculate the bounds, and therefore length of
//! the winning range of seconds to hold before releasing the boat. [`find_product_of_races`] can be used for both
//! parts, as the single race is unchanged by `iter().product`.
use std::fs;
/// A race duration, with the distance to beat in that time
#[derive(Eq, PartialEq, Debug)]
struct Race {
duration: i64,
distance_record: i64,
}
impl Race {
fn new(time: i64, distance_record: i64) -> Race {
Race {
duration: time,
distance_record,
}
}
}
/// The entry point for running the solutions with the 'real' puzzle input.
///
/// - The puzzle input is expected to be at `<project_root>/res/day-6-input`
/// - It is expected this will be called by [`super::main()`] when the user elects to run day 6.
pub fn run() {
let contents = fs::read_to_string("res/day-6-input.txt").expect("Failed to read file");
println!(
"The product of the number of ways to win is: {}",
find_product_of_races(&parse_input(&contents, part_1_line_parser))
);
println!(
"The number of ways to win the combined race is: {}",
find_product_of_races(&parse_input(&contents, part_2_line_parser))
);
}
/// Parse input from a line of durations and a line current record best times into a
/// list of records. How to parse each line is abstracted to a `line_parser` for each part
fn parse_input(input: &String, line_parser: fn(&str) -> Vec<i64>) -> Vec<Race> {
let mut lines = input.split("\n");
line_parser(lines.next().unwrap())
.iter()
.zip(line_parser(lines.next().unwrap()))
.map(|(&t, d)| Race::new(t, d))
.collect()
}
/// Parse lines as multiple numbers separated by whitespace
fn part_1_line_parser(line: &str) -> Vec<i64> {
line.split(" ")
.filter_map(|word| word.parse().ok())
.collect()
}
/// Parse lines as a single number each
fn part_2_line_parser(line: &str) -> Vec<i64> {
let num = line
.chars()
.filter_map(|chr| chr.to_digit(10))
.fold(0i64, |acc, digit| acc * 10 + digit as i64);
return vec![num];
}
/// Convert a list of races into the size of the range of hold times, and find the product of these as the puzzle
/// answer.
fn find_product_of_races(races: &Vec<Race>) -> i64 {
races.iter().map(find_count_of_winning_hold_times).product()
}
/// Calculate the range of seconds the boat's button could be pressed for to exceed the current record for a race.
/// Uses the [quadratic formula](https://en.wikipedia.org/wiki/Quadratic_formula) to calculate the upper and lower
/// bound, and return the size of the range of integers within those bounds.
fn find_count_of_winning_hold_times(race: &Race) -> i64 {
// `sqrt` and `/` expect to work with floats
let duration = race.duration as f64;
let record = race.distance_record as f64;
let root_a = (duration + (duration.powf(2.0) - 4.0 * record).sqrt()) / 2.0;
let root_b = (duration - (duration.powf(2.0) - 4.0 * record).sqrt()) / 2.0;
// Inclusive. Floor here rounds down the the last excluded integer before the range, so then add one to get the
// inclusive lower bound.
let lower_bound = root_a.min(root_b).floor() as i64 + 1;
// Exclusive. Ceil always gives the next integer beyond the range.
let upper_bound = root_a.max(root_b).ceil() as i64;
upper_bound - lower_bound
}
#[cfg(test)]
mod tests {
use crate::day_6::*;
#[test]
fn can_parse_input_for_part_1() {
assert_eq!(
parse_input(&example_input(), part_1_line_parser),
vec![Race::new(7, 9), Race::new(15, 40), Race::new(30, 200)]
);
}
#[test]
fn can_parse_input_for_part_2() {
assert_eq!(
parse_input(&example_input(), part_2_line_parser),
vec![Race::new(71530, 940200)]
);
}
fn example_input() -> String {
"\
Time: 7 15 30
Distance: 9 40 200
"
.to_string()
}
#[test]
fn can_find_count_of_winning_hold_times() {
assert_eq!(find_count_of_winning_hold_times(&Race::new(7, 9)), 4);
assert_eq!(find_count_of_winning_hold_times(&Race::new(15, 40)), 8);
assert_eq!(find_count_of_winning_hold_times(&Race::new(30, 200)), 9);
}
#[test]
fn can_find_product_of_winning_hold_times() {
assert_eq!(
find_product_of_races(&parse_input(&example_input(), part_1_line_parser)),
288
);
}
#[test]
fn can_find_hold_times_for_combined_race() {
assert_eq!(
find_product_of_races(&parse_input(&example_input(), part_2_line_parser)),
71503
);
}
}