diff --git a/src/day12.rs b/src/day12.rs new file mode 100644 index 0000000..fdf8047 --- /dev/null +++ b/src/day12.rs @@ -0,0 +1,129 @@ +use crate::util::maps::*; + +const DISCOUNT: bool = true; + +pub fn day12() { + let mut plots: Map = Map::from_string(&crate::input(12)); + + let mut regions: Vec = vec![]; + for (x, y) in plots.coordinates() { + if plots.get(x, y) != Some('.') { + let region = Region::new(x, y, &plots); + for (x, y) in plots.coordinates() { + if region.tiles.get(x, y).unwrap() { + plots.set(x, y, '.'); + } + } + regions.push(region); + } + } + + let mut total_cost: u64 = 0; + for region in regions { + let mut sides: u64 = 0; + let mut borders = region.borders.clone(); + + while let Some(border) = borders.pop() { + let mut combined = vec![border]; + + while let Some(pos) = borders + .iter() + .position(|other| combined.iter().any(|b| b.contiguous_straight(other))) + { + combined.push(borders.remove(pos)); + } + + sides += 1; + } + + let area = region.tiles.iter().filter(|&&t| t).count() as u64; + + total_cost += if DISCOUNT { + area * sides + } else { + area * region.borders.len() as u64 + }; + } + + println!("Total Cost: {}", total_cost); +} + +#[derive(PartialEq, Eq, Clone)] +struct Border { + x: u64, + y: u64, + facing: Direction, +} + +impl Border { + fn new(x: u64, y: u64, facing: Direction) -> Self { + Self { x, y, facing } + } + + fn contiguous_straight(&self, other: &Self) -> bool { + if self.facing != other.facing { + return false; + } + + if self.facing == Direction::North || self.facing == Direction::South { + return self.y == other.y && self.x.max(other.x) - self.x.min(other.x) == 1; + } + if self.facing == Direction::West || self.facing == Direction::East { + return self.x == other.x && self.y.max(other.y) - self.y.min(other.y) == 1; + } + + unreachable!(); + } +} + +struct Region { + pub borders: Vec, + pub tiles: Map, +} + +impl Region { + fn new(x: usize, y: usize, plots: &Map) -> Self { + let c = plots.get(x, y).unwrap(); + let mut tiles = Map::::from_dimensions(plots.width(), plots.height(), false); + + fn collect_plots( + c: char, + x: usize, + y: usize, + plots: &Map, + tiles: &mut Map, + ) -> Vec<(usize, usize)> { + if plots.get(x, y) != Some(c) || tiles.get(x, y) == Some(true) { + return vec![]; + } + + tiles.set(x, y, true); + + let mut region = vec![(x, y)]; + for (nx, ny) in Direction::all_variants() + .into_iter() + .filter_map(|d| plots.travel(x, y, d)) + { + region.extend(collect_plots(c, nx, ny, plots, tiles)); + } + + region + } + + let collected_plots = collect_plots(c, x, y, plots, &mut tiles); + + let borders = collected_plots + .iter() + .flat_map(|&(x, y)| { + Direction::all_variants().into_iter().filter_map(move |d| { + match plots.travel_get(x, y, d) { + Some(o) if o == c => None, + _ => Some(Border::new(x as u64, y as u64, d)), + } + }) + }) + .collect(); + + Self { borders, tiles } + } +} diff --git a/src/main.rs b/src/main.rs index e8e0ef8..05f718e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +pub mod util; + #[allow(dead_code)] mod day1; #[allow(dead_code)] @@ -18,10 +20,12 @@ mod day8; mod day9; #[allow(dead_code)] mod day10; +#[allow(dead_code)] mod day11; +mod day12; fn main() { - day11::day11(); + day12::day12(); } pub fn input(day: u8) -> String { diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..a98b9c8 --- /dev/null +++ b/src/util.rs @@ -0,0 +1 @@ +pub mod maps; diff --git a/src/util/maps.rs b/src/util/maps.rs new file mode 100644 index 0000000..55072f8 --- /dev/null +++ b/src/util/maps.rs @@ -0,0 +1,108 @@ +use itertools::Itertools; + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Direction { + North, + South, + East, + West, +} + +impl Direction { + pub fn all_variants() -> Vec { + vec![Self::North, Self::East, Self::West, Self::South] + } +} + +#[derive(Clone)] +pub struct Map +where + T: Copy, +{ + pub map: Vec>, +} + +impl Map { + pub fn from_string(string: &str) -> Self { + Map::from_2d_vec( + string + .lines() + .filter(|line| !line.is_empty()) + .map(|line| line.chars().collect()) + .collect(), + ) + } +} + +impl Map +where + T: Copy, +{ + pub fn from_2d_vec(map: Vec>) -> Self { + Self { map } + } + + pub fn from_dimensions(width: usize, height: usize, default: T) -> Self { + Self { + map: vec![vec![default; width]; height], + } + } + + pub fn travel(&self, x: usize, y: usize, direction: Direction) -> Option<(usize, usize)> { + match direction { + Direction::North => Some((x, y.checked_sub(1)?)), + Direction::South => { + if y + 1 < self.height() { + Some((x, y + 1)) + } else { + None + } + } + Direction::West => Some((x.checked_sub(1)?, y)), + Direction::East => { + if x + 1 < self.width() { + Some((x + 1, y)) + } else { + None + } + } + } + } + + pub fn travel_get(&self, x: usize, y: usize, direction: Direction) -> Option { + let (nx, ny) = self.travel(x, y, direction)?; + self.get(nx, ny) + } + + pub fn get(&self, x: usize, y: usize) -> Option { + Some(*self.map.get(y)?.get(x)?) + } + + pub fn set(&mut self, x: usize, y: usize, value: T) { + self.map[y][x] = value + } + + pub fn height(&self) -> usize { + self.map.len() + } + + pub fn width(&self) -> usize { + self.map[0].len() + } + + pub fn in_bounds(&self, x: usize, y: usize) -> bool { + x < self.width() && y < self.height() + } + + pub fn iter(&self) -> impl Iterator { + self.map.iter().flat_map(|row| row.iter()) + } + + pub fn coordinates(&self) -> impl Iterator { + (0..self.width()).cartesian_product(0..self.height()) + } + + pub fn enumerate(&self) -> impl Iterator { + self.coordinates().map(|(x, y)| (x, y, &self.map[y][x])) + } +}