diff --git a/2023/benches/days.rs b/2023/benches/days.rs index d079062..8662026 100644 --- a/2023/benches/days.rs +++ b/2023/benches/days.rs @@ -9,7 +9,7 @@ use criterion::Criterion; use aoc_2023::get_implementation; /// Number of days we have an implementation to benchmark -const DAYS_IMPLEMENTED: u8 = 2; +const DAYS_IMPLEMENTED: u8 = 3; fn read_input(day: u8) -> std::io::Result> { let input_path = format!("inputs/{day:02}.txt"); diff --git a/2023/src/day03.rs b/2023/src/day03.rs index 7c1760f..b531ef8 100644 --- a/2023/src/day03.rs +++ b/2023/src/day03.rs @@ -1,7 +1,184 @@ -pub fn part1(_input: &[u8]) -> anyhow::Result { - anyhow::bail!("Not implemented") +use std::collections::HashMap; +use std::ops::Index; + +use anyhow::Context; + +struct Grid<'a> { + width: usize, + data: &'a [u8], } -pub fn part2(_input: &[u8]) -> anyhow::Result { - anyhow::bail!("Not implemented") +impl<'a> Grid<'a> { + pub fn new(data: &'a [u8]) -> anyhow::Result { + let width = 1 + data + .iter() + .position(|&c| c == b'\n') + .context("Failed to find end of line in grid")?; + + anyhow::ensure!( + data.len() % width == 0, + "Grid should divide equally into rows" + ); + + Ok(Self { width, data }) + } + + pub fn height(&self) -> usize { + self.data.len() / self.width + } + + pub fn width(&self) -> usize { + self.width - 1 + } + + pub fn rows(&self) -> impl Iterator { + let width = self.width(); + self.data + .chunks_exact(self.width) + .map(move |row| &row[..width]) + } +} + +impl<'a> Index for Grid<'a> { + type Output = [u8]; + + fn index(&self, y: usize) -> &Self::Output { + let offset = y * self.width; + &self.data[offset..(offset + self.width())] + } +} + +fn is_surrounded(grid: &Grid<'_>, y: usize, start: usize, last: usize) -> bool { + fn is_symbol(c: u8) -> bool { + match c { + b'0'..=b'9' => false, + b'.' => false, + b'\n' => false, + _ => true, + } + } + let x_min = start.saturating_sub(1); + let x_max = Ord::min(grid.width(), last + 2); + + if y > 0 { + for nx in x_min..x_max { + if is_symbol(grid[y - 1][nx]) { + return true; + } + } + } + + if y + 1 < grid.height() { + for nx in x_min..x_max { + if is_symbol(grid[y + 1][nx]) { + return true; + } + } + } + + is_symbol(grid[y][x_min]) || is_symbol(grid[y][x_max - 1]) +} + +pub fn part1(input: &[u8]) -> anyhow::Result { + let grid = Grid::new(input)?; + let mut sum = 0; + + for (y, row) in grid.rows().enumerate() { + let mut it = row.iter().enumerate(); + while let Some((start, &c)) = it.find(|&(_, c)| c.is_ascii_digit()) { + let mut end = start; + let mut num = u32::from(c - b'0'); + + for (x, &c) in it.by_ref().take_while(|&(_, c)| c.is_ascii_digit()) { + end = x; + num *= 10; + num += u32::from(c - b'0'); + } + + if is_surrounded(&grid, y, start, end) { + sum += num; + } + } + } + + Ok(sum.to_string()) +} + +fn find_gear(grid: &Grid<'_>, y: usize, start: usize, end: usize) -> Option<(usize, usize)> { + let x_min = start.saturating_sub(1); + let x_max = Ord::min(grid.width(), end + 2); + + if y > 0 { + for nx in x_min..x_max { + if grid[y - 1][nx] == b'*' { + return Some((nx, y - 1)); + } + } + } + + if y + 1 < grid.height() { + for nx in x_min..x_max { + if grid[y + 1][nx] == b'*' { + return Some((nx, y + 1)); + } + } + } + + for nx in [x_min, x_max - 1] { + if grid[y][nx] == b'*' { + return Some((nx, y)); + } + } + + None +} + +pub fn part2(input: &[u8]) -> anyhow::Result { + let grid = Grid::new(input)?; + let mut gears: HashMap<(usize, usize), (u32, u32)> = HashMap::new(); + + for (y, row) in grid.rows().enumerate() { + let mut it = row.iter().enumerate(); + while let Some((start, &c)) = it.find(|&(_, c)| c.is_ascii_digit()) { + let mut end = start; + let mut num = u32::from(c - b'0'); + + for (x, &c) in it.by_ref().take_while(|&(_, c)| c.is_ascii_digit()) { + end = x; + num *= 10; + num += u32::from(c - b'0'); + } + + // Assumption: there is only one gear per number. This turns out to be true. + if let Some((gear_x, gear_y)) = find_gear(&grid, y, start, end) { + let entry = gears.entry((gear_x, gear_y)).or_insert((0, 1)); + entry.0 += 1; + entry.1 *= num; + } + } + } + + let sum: u32 = gears + .into_values() + .filter_map(|(count, ratio)| if count == 2 { Some(ratio) } else { None }) + .sum(); + + Ok(sum.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE: &[u8] = include_bytes!("samples/03.txt"); + + #[test] + fn sample_part1() { + assert_eq!(part1(SAMPLE).unwrap(), "4361"); + } + + #[test] + fn sample_part2() { + assert_eq!(part2(SAMPLE).unwrap(), "467835"); + } }