summaryrefslogtreecommitdiff
path: root/2023/src/bin/day_21.rs
diff options
context:
space:
mode:
Diffstat (limited to '2023/src/bin/day_21.rs')
-rw-r--r--2023/src/bin/day_21.rs395
1 files changed, 395 insertions, 0 deletions
diff --git a/2023/src/bin/day_21.rs b/2023/src/bin/day_21.rs
new file mode 100644
index 0000000..9ef273e
--- /dev/null
+++ b/2023/src/bin/day_21.rs
@@ -0,0 +1,395 @@
+use nom::{
+ branch::alt,
+ character::complete::{char, line_ending},
+ combinator::{map, value},
+ multi::{many1, separated_list1},
+ IResult,
+};
+use std::{collections::BTreeSet, fs, mem};
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let input = fs::read_to_string("inputs/day_21.txt")?;
+ let garden = WalledGarden::parser(&input).unwrap().1;
+
+ dbg!(garden.count_closed_walls_walkable_after_steps(garden.center(), 64));
+ dbg!(garden.count_open_walls_walkable_after_steps(26501365));
+
+ Ok(())
+}
+
+#[derive(Debug, Clone)]
+struct WalledGarden {
+ rocks: Vec<Vec<bool>>,
+ walkable_to: Vec<Vec<bool>>,
+ walkable_to_back: Vec<Vec<bool>>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum WalledGardenState {
+ Empty,
+ Walkable,
+ Rock,
+}
+
+#[derive(Debug)]
+struct MaxWalkable {
+ odd_steps_max: usize,
+ even_steps_max: usize,
+ min_steps: usize,
+}
+
+#[derive(Debug)]
+struct EntryPoint {
+ entry: (usize, usize),
+ max: MaxWalkable,
+}
+
+impl WalledGarden {
+ fn new(tiles: Vec<Vec<WalledGardenState>>) -> WalledGarden {
+ let rocks: Vec<Vec<bool>> = tiles
+ .iter()
+ .map(|row| {
+ row.iter()
+ .map(|t| *t == WalledGardenState::Rock)
+ .collect::<Vec<bool>>()
+ })
+ .collect();
+ let walkable_to: Vec<Vec<bool>> = vec![vec![false; rocks[0].len()]; rocks.len()];
+
+ WalledGarden {
+ rocks,
+ walkable_to_back: walkable_to.clone(),
+ walkable_to: walkable_to.clone(),
+ }
+ }
+
+ fn parser(input: &str) -> IResult<&str, Self> {
+ map(
+ separated_list1(line_ending, many1(WalledGardenState::parser)),
+ WalledGarden::new,
+ )(input)
+ }
+
+ fn count_closed_walls_walkable_after_steps(
+ &self,
+ start: (usize, usize),
+ steps: usize,
+ ) -> usize {
+ let mut garden = self.clone();
+ garden.walkable_to[start.1][start.0] = true;
+ for _ in 0..steps {
+ garden.closed_walls_next_mut();
+ }
+ garden.count_walkable()
+ }
+
+ fn closed_walls_find_max_walkable(&self, start: (usize, usize)) -> MaxWalkable {
+ let mut odd_steps_max = 0;
+ let mut even_steps_max = 0;
+
+ let mut garden = self.clone();
+ garden.walkable_to[start.1][start.0] = true;
+
+ let mut unchanged_count = 0;
+ for i in 1.. {
+ garden.closed_walls_next_mut();
+
+ if i % 2 == 0 {
+ let new_even_max_countable = garden.count_walkable();
+ if even_steps_max == new_even_max_countable {
+ unchanged_count += 1;
+ } else {
+ even_steps_max = new_even_max_countable;
+ }
+ } else {
+ let new_odd_max_countable = garden.count_walkable();
+ if odd_steps_max == new_odd_max_countable {
+ unchanged_count += 1;
+ } else {
+ odd_steps_max = new_odd_max_countable;
+ }
+ }
+
+ if unchanged_count > 1 {
+ return MaxWalkable {
+ odd_steps_max,
+ even_steps_max,
+ min_steps: i,
+ };
+ }
+ }
+ unreachable!()
+ }
+
+ fn closed_walls_next_mut(&mut self) {
+ for (y, row) in self.walkable_to_back.iter_mut().enumerate() {
+ for (x, tile) in row.iter_mut().enumerate() {
+ if !self.rocks[y][x] {
+ *tile = (y > 0 && self.walkable_to[y - 1][x])
+ || (y < self.walkable_to.len() - 1 && self.walkable_to[y + 1][x])
+ || (x > 0 && self.walkable_to[y][x - 1])
+ || (x < self.walkable_to[y].len() - 1 && self.walkable_to[y][x + 1]);
+ }
+ }
+ }
+
+ mem::swap(&mut self.walkable_to, &mut self.walkable_to_back);
+ }
+
+ fn count_walkable(&self) -> usize {
+ self.walkable_to
+ .iter()
+ .flat_map(|row| row.iter())
+ .filter(|s| **s)
+ .count()
+ }
+
+ fn count_open_walls_walkable_after_steps(&self, steps: usize) -> usize {
+ // assumptions:
+ // - this field is square
+ // - there is a direct path from the starting point up, down, left, and right
+ // - there is a direct path around the edges
+ // - the start is in the center
+
+ let (size_x, size_y) = self.size();
+ let (center_x, center_y) = self.center();
+
+ let max_chunks_deviance = if (steps - center_y) % size_y > 0 {
+ 1 + (steps - center_y) / size_y
+ } else {
+ (steps - center_y) / size_y
+ };
+
+ let mut total_walkable = 0;
+
+ for quadrant in [
+ EntryPoint::new((0, 0), &self),
+ EntryPoint::new((size_x - 1, 0), &self),
+ EntryPoint::new((0, size_y - 1), &self),
+ EntryPoint::new((size_x - 1, size_y - 1), &self),
+ ] {
+ let steps_to_quadrant_alignment = center_x + center_y + 2;
+
+ let mut distance_from_edge = 0;
+ while max_chunks_deviance > distance_from_edge {
+ let steps_from_alignment_to_target_chunk =
+ (max_chunks_deviance - distance_from_edge - 1) * size_y;
+ if steps_to_quadrant_alignment + steps_from_alignment_to_target_chunk > steps {
+ distance_from_edge += 1;
+ continue;
+ }
+ let steps_in_chunk =
+ steps - steps_to_quadrant_alignment - steps_from_alignment_to_target_chunk;
+ if steps_in_chunk >= quadrant.max.min_steps {
+ break;
+ }
+
+ let walkable_per_chunk =
+ self.count_closed_walls_walkable_after_steps(quadrant.entry, steps_in_chunk);
+ total_walkable += walkable_per_chunk * (max_chunks_deviance - distance_from_edge);
+ distance_from_edge += 1;
+ }
+
+ let remaining_diagonals = max_chunks_deviance - distance_from_edge;
+ let even_length_diagonals = remaining_diagonals / 2;
+ let odd_length_diagonals = even_length_diagonals + remaining_diagonals % 2;
+
+ let even_length_diagonal_chunks = even_length_diagonals * (even_length_diagonals + 1);
+ let odd_length_diagonal_chunks = odd_length_diagonals.pow(2);
+
+ let odd_diagonal_has_even_steps_left = (steps - steps_to_quadrant_alignment) % 2 == 0;
+ total_walkable += if odd_diagonal_has_even_steps_left {
+ odd_length_diagonal_chunks * quadrant.max.even_steps_max
+ + even_length_diagonal_chunks * quadrant.max.odd_steps_max
+ } else {
+ even_length_diagonal_chunks * quadrant.max.even_steps_max
+ + odd_length_diagonal_chunks * quadrant.max.odd_steps_max
+ };
+ }
+
+ for cardinal in [
+ EntryPoint::new((0, center_y), &self),
+ EntryPoint::new((center_x, 0), &self),
+ EntryPoint::new((size_x - 1, center_y), &self),
+ EntryPoint::new((center_x, size_y - 1), &self),
+ ] {
+ let steps_to_cardinal_alignment = center_y + 1;
+
+ let mut distance_from_edge = 0;
+ while max_chunks_deviance > distance_from_edge {
+ let steps_from_alignment_to_target_chunk =
+ (max_chunks_deviance - distance_from_edge - 1) * size_y;
+ let steps_in_chunk =
+ steps - steps_to_cardinal_alignment - steps_from_alignment_to_target_chunk;
+ if steps_in_chunk >= cardinal.max.min_steps {
+ break;
+ }
+
+ let walkable_per_chunk =
+ self.count_closed_walls_walkable_after_steps(cardinal.entry, steps_in_chunk);
+ total_walkable += walkable_per_chunk;
+ distance_from_edge += 1;
+ }
+
+ let remaining_chunks = max_chunks_deviance - distance_from_edge;
+ let even_index_chunks = remaining_chunks / 2;
+ let odd_index_chunks = even_index_chunks + remaining_chunks % 2;
+
+ let odd_chunk_has_even_steps_left = (steps - steps_to_cardinal_alignment) % 2 == 0;
+ total_walkable += if odd_chunk_has_even_steps_left {
+ odd_index_chunks * cardinal.max.even_steps_max
+ + even_index_chunks * cardinal.max.odd_steps_max
+ } else {
+ even_index_chunks * cardinal.max.even_steps_max
+ + odd_index_chunks * cardinal.max.odd_steps_max
+ };
+ }
+
+ for center in [EntryPoint::new((center_x, center_y), &self)] {
+ total_walkable += if steps >= center.max.min_steps {
+ if steps % 2 == 0 {
+ center.max.even_steps_max
+ } else {
+ center.max.odd_steps_max
+ }
+ } else {
+ self.count_closed_walls_walkable_after_steps(center.entry, steps)
+ };
+ }
+
+ total_walkable
+ }
+
+ fn size(&self) -> (usize, usize) {
+ (self.rocks[0].len(), self.rocks.len())
+ }
+
+ fn center(&self) -> (usize, usize) {
+ let (size_x, size_y) = self.size();
+ (size_x / 2, size_y / 2)
+ }
+}
+
+impl WalledGardenState {
+ fn parser(input: &str) -> IResult<&str, Self> {
+ alt((
+ value(WalledGardenState::Empty, char('.')),
+ value(WalledGardenState::Walkable, char('S')),
+ value(WalledGardenState::Rock, char('#')),
+ ))(input)
+ }
+}
+
+impl EntryPoint {
+ fn new(entry: (usize, usize), garden: &WalledGarden) -> EntryPoint {
+ EntryPoint {
+ max: garden.closed_walls_find_max_walkable(entry),
+ entry,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+struct OpenGarden {
+ rockmap_size: (isize, isize),
+ rocks: BTreeSet<(isize, isize)>,
+ walkable: BTreeSet<(isize, isize)>,
+}
+
+impl OpenGarden {
+ fn parser(input: &str) -> IResult<&str, Self> {
+ map(
+ separated_list1(line_ending, many1(WalledGardenState::parser)),
+ |walled_garden_map| OpenGarden {
+ rockmap_size: (
+ walled_garden_map.len() as isize,
+ walled_garden_map[0].len() as isize,
+ ),
+ rocks: walled_garden_map
+ .iter()
+ .enumerate()
+ .flat_map(|(y, row)| {
+ row.iter().enumerate().filter_map(move |(x, s)| {
+ (*s == WalledGardenState::Rock).then(|| (y as isize, x as isize))
+ })
+ })
+ .collect(),
+ walkable: walled_garden_map
+ .iter()
+ .enumerate()
+ .flat_map(|(y, row)| {
+ row.iter().enumerate().filter_map(move |(x, s)| {
+ (*s == WalledGardenState::Walkable).then(|| (y as isize, x as isize))
+ })
+ })
+ .collect(),
+ },
+ )(input)
+ }
+
+ fn count_open_walls_walkable_after_steps(&self, steps: usize) -> usize {
+ let mut garden = self.clone();
+ for _ in 0..steps {
+ garden.next_mut();
+ }
+ garden.count_walkable()
+ }
+
+ fn next_mut(&mut self) {
+ let walkable = mem::take(&mut self.walkable);
+ self.walkable = walkable
+ .iter()
+ .flat_map(|(y, x)| [(y - 1, *x), (y + 1, *x), (*y, x - 1), (*y, x + 1)])
+ .filter(|(y, x)| !self.is_rock(*y, *x))
+ .collect();
+ }
+
+ fn is_rock(&self, y: isize, x: isize) -> bool {
+ let y = y.rem_euclid(self.rockmap_size.0);
+ let x = x.rem_euclid(self.rockmap_size.1);
+ self.rocks.contains(&(y, x))
+ }
+
+ fn count_walkable(&self) -> usize {
+ self.walkable.len()
+ }
+}
+
+#[test]
+fn open_matches_optimized_for_small_steps() {
+ let input = fs::read_to_string("inputs/day_21.txt").unwrap();
+ let walled_garden = WalledGarden::parser(&input).unwrap().1;
+ let open_garden = OpenGarden::parser(&input).unwrap().1;
+
+ let steps = 132;
+ assert_eq!(
+ walled_garden.count_open_walls_walkable_after_steps(steps),
+ open_garden.count_open_walls_walkable_after_steps(steps)
+ );
+}
+
+#[test]
+fn open_matches_optimized_for_medium_steps() {
+ let input = fs::read_to_string("inputs/day_21.txt").unwrap();
+ let walled_garden = WalledGarden::parser(&input).unwrap().1;
+ let open_garden = OpenGarden::parser(&input).unwrap().1;
+
+ let steps = 65 + 132;
+ assert_eq!(
+ walled_garden.count_open_walls_walkable_after_steps(steps),
+ open_garden.count_open_walls_walkable_after_steps(steps)
+ );
+}
+
+#[test]
+fn open_matches_optimized_for_bigger_steps() {
+ let input = fs::read_to_string("inputs/day_21.txt").unwrap();
+ let walled_garden = WalledGarden::parser(&input).unwrap().1;
+ let open_garden = OpenGarden::parser(&input).unwrap().1;
+
+ let steps = 270;
+ assert_eq!(
+ walled_garden.count_open_walls_walkable_after_steps(steps),
+ open_garden.count_open_walls_walkable_after_steps(steps)
+ );
+}