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}