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}