aoc/year2023/
day21.rs

1//! # Step Counter
2//!
3//! This solution uses a geometric approach. Looking at the input data reveals several crucial
4//! insights:
5//!
6//! * The sample data is a decoy and will not work with this solution.
7//! * The real input data has two special properties:
8//!     * Vertical and horizontal "roads" run from the center.
9//!     * The edge of the input is completely free of obstructions.
10//!
11//! These properties mean that we can always cross a tile in exactly 131 steps. We start in the
12//! middle of a tile and need 65 steps to reach the edge. Part two asks how many plots can be
13//! reached in 26501365 steps.
14//!
15//! ```none
16//!     26501365 => 65 + 131 * n => n = 202300
17//! ```
18//!
19//! The number of tiles that we can reach forms a rough diamond 202300 tiles wide.
20//! For example `n = 2` looks like:
21//!
22//! ```none
23//!       #
24//!      ###
25//!     #####
26//!      ###
27//!       #
28//! ```
29//!
30//! The next insight is that if we can reach a plot in `x` steps the we can also reach it in
31//! `x + 2, x + 4...` steps by repeatedly stepping back and forth 1 tile. This means the
32//! number of tiles reachable depends on the *parity* of a plot from the center,
33//! i.e. whether it is an odd or even number of steps. As the 131 width of the tile is an odd
34//! number of plots, the number of plots reachable flips from odd to even each time we cross a
35//! whole tile. There are `n²` even plots and `(n + 1)²` odd plots in the diamond.
36//!
37//! ```none
38//!       O
39//!      OEO
40//!     OEOEO
41//!      OEO
42//!       O
43//! ```
44//!
45//! Lastly, we can only partially reach some tiles on the edges. Solid triangles represent corners
46//! that can be reached and hollow triangle represents corners that are too far away.
47//!
48//! ```none
49//!          ┌--┐
50//!          |◸◹|
51//!         ◢|  |◣
52//!       ┌--┼--┼--┐
53//!       |◸ |  | ◹|
54//!      ◢|  |  |  |◣
55//!    ┌--┼--┼--┼--┼--┐
56//!    |◸ |  |  |  | ◹|
57//!    |◺ |  |  |  | ◿|
58//!    └--┼--┼--┼--┼--┘
59//!      ◥|  |  |  |◤
60//!       |◺ |  | ◿|
61//!       └--┼--┼--┘
62//!         ◥|  |◤
63//!          |◺◿|
64//!          └--┘
65//! ```
66//!
67//! The total area is adjusted by:
68//! * Adding `n` extra even corners
69//!     ```none
70//!         ◤◥
71//!         ◣◢
72//!     ```
73//! * Subtracting `n + 1` odd corners
74//!     ```none
75//!         ◸◹
76//!         ◺◿
77//!     ```
78//!
79//! To find the values for the total number of odd, even plots and the unreachable odd corners
80//! we BFS from the center tile, counting odd and even plots separately. Any plots more than
81//! 65 steps from the center will be unreachable at the edges of the diamond.
82//!
83//! One nuance is that to always correctly find the extra reachable even corner plots requires a
84//! *second* BFS starting from the corners and working inwards. All tiles within 64 steps are
85//! reachable at the edges of the diamond. For some inputs this happens to be the same as the number
86//! of tiles greater than 65 steps from the center by coincidence, however this is not guaranteed so
87//! a second BFS is more reliable solution.
88use crate::util::grid::*;
89use crate::util::point::*;
90use std::collections::VecDeque;
91
92const CENTER: Point = Point::new(65, 65);
93const CORNERS: [Point; 4] =
94    [Point::new(0, 0), Point::new(130, 0), Point::new(0, 130), Point::new(130, 130)];
95
96type Input = (u64, u64);
97
98pub fn parse(input: &str) -> Input {
99    let grid = Grid::parse(input);
100
101    // Search from the center tile outwards.
102    let (even_inner, even_outer, odd_inner, odd_outer) = bfs(&grid, &[CENTER], 130);
103    let part_one = even_inner;
104    let even_full = even_inner + even_outer;
105    let odd_full = odd_inner + odd_outer;
106    let remove_corners = odd_outer;
107
108    // Search from the 4 corners inwards.
109    let (even_inner, ..) = bfs(&grid, &CORNERS, 64);
110    let add_corners = even_inner;
111
112    // Sum the components of the diamond.
113    let n = 202300;
114    let first = n * n * even_full;
115    let second = (n + 1) * (n + 1) * odd_full;
116    let third = n * add_corners;
117    let fourth = (n + 1) * remove_corners;
118    let part_two = first + second + third - fourth;
119
120    (part_one, part_two)
121}
122
123pub fn part1(input: &Input) -> u64 {
124    input.0
125}
126
127pub fn part2(input: &Input) -> u64 {
128    input.1
129}
130
131/// Breadth first search from any number of starting locations with a limit on maximum steps.
132fn bfs(grid: &Grid<u8>, starts: &[Point], limit: u32) -> (u64, u64, u64, u64) {
133    let mut grid = grid.clone();
134    let mut todo = VecDeque::new();
135
136    let mut even_inner = 0;
137    let mut even_outer = 0;
138    let mut odd_inner = 0;
139    let mut odd_outer = 0;
140
141    for &start in starts {
142        grid[start] = b'#';
143        todo.push_back((start, 0));
144    }
145
146    while let Some((position, cost)) = todo.pop_front() {
147        // First split by odd or even parity then by distance from the starting point.
148        if cost % 2 == 1 {
149            if position.manhattan(CENTER) <= 65 {
150                odd_inner += 1;
151            } else {
152                odd_outer += 1;
153            }
154        } else if cost <= 64 {
155            even_inner += 1;
156        } else {
157            even_outer += 1;
158        }
159
160        if cost < limit {
161            for next in ORTHOGONAL.map(|o| position + o) {
162                if grid.contains(next) && grid[next] != b'#' {
163                    grid[next] = b'#';
164                    todo.push_back((next, cost + 1));
165                }
166            }
167        }
168    }
169
170    (even_inner, even_outer, odd_inner, odd_outer)
171}