Skip to main content

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 then 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 triangles represent 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 a more reliable solution.
88use std::collections::VecDeque;
89
90use crate::util::grid::*;
91use crate::util::point::*;
92
93const CENTER: Point = Point::new(66, 66);
94const CORNERS: [Point; 4] =
95    [Point::new(1, 1), Point::new(131, 1), Point::new(1, 131), Point::new(131, 131)];
96
97type Input = (u64, u64);
98
99pub fn parse(input: &str) -> Input {
100    // A newline border allows us to avoid boundary checks.
101    let grid = Grid::parse_with_border(input);
102
103    // Search from the center tile outwards.
104    let (even_inner, even_outer, odd_inner, odd_outer) = bfs(&grid, &[CENTER], 130);
105    let part_one = even_inner;
106    let even_full = even_inner + even_outer;
107    let odd_full = odd_inner + odd_outer;
108    let remove_corners = odd_outer;
109
110    // Search from the 4 corners inwards.
111    let (even_inner, ..) = bfs(&grid, &CORNERS, 64);
112    let add_corners = even_inner;
113
114    // Sum the components of the diamond.
115    let n = 202300;
116    let first = n * n * even_full;
117    let second = (n + 1) * (n + 1) * odd_full;
118    let third = n * add_corners;
119    let fourth = (n + 1) * remove_corners;
120    let part_two = first + second + third - fourth;
121
122    (part_one, part_two)
123}
124
125pub fn part1(input: &Input) -> u64 {
126    input.0
127}
128
129pub fn part2(input: &Input) -> u64 {
130    input.1
131}
132
133/// Breadth-first search from any number of starting locations with a limit on maximum steps.
134fn bfs(grid: &Grid<u8>, starts: &[Point], limit: u32) -> (u64, u64, u64, u64) {
135    let mut grid = grid.clone();
136    let mut todo = VecDeque::new();
137
138    let mut even_inner = 0;
139    let mut even_outer = 0;
140    let mut odd_inner = 0;
141    let mut odd_outer = 0;
142
143    for &start in starts {
144        grid[start] = b'#';
145        todo.push_back((start, 0));
146    }
147
148    while let Some((position, cost)) = todo.pop_front() {
149        // First split by odd or even parity then by distance from the starting point.
150        if cost % 2 == 1 {
151            if position.manhattan(CENTER) <= 65 {
152                odd_inner += 1;
153            } else {
154                odd_outer += 1;
155            }
156        } else if cost <= 64 {
157            even_inner += 1;
158        } else {
159            even_outer += 1;
160        }
161
162        if cost < limit {
163            for next in ORTHOGONAL.map(|o| position + o) {
164                if grid[next] == b'.' {
165                    grid[next] = b'#';
166                    todo.push_back((next, cost + 1));
167                }
168            }
169        }
170    }
171
172    (even_inner, even_outer, odd_inner, odd_outer)
173}