Skip to content

Commit b9869b7

Browse files
committed
Document day 21
1 parent d066d29 commit b9869b7

File tree

1 file changed

+92
-0
lines changed

1 file changed

+92
-0
lines changed

src/year2023/day21.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,90 @@
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.
188
use crate::util::grid::*;
289
use crate::util::point::*;
390
use std::collections::VecDeque;
@@ -11,15 +98,18 @@ type Input = (u64, u64);
1198
pub fn parse(input: &str) -> Input {
1299
let grid = Grid::parse(input);
13100

101+
// Search from the center tile outwards.
14102
let (even_inner, even_outer, odd_inner, odd_outer) = bfs(&grid, &[CENTER], 130);
15103
let part_one = even_inner;
16104
let even_full = even_inner + even_outer;
17105
let odd_full = odd_inner + odd_outer;
18106
let remove_corners = odd_outer;
19107

108+
// Search from the 4 corners inwards.
20109
let (even_inner, ..) = bfs(&grid, &CORNERS, 64);
21110
let add_corners = even_inner;
22111

112+
// Sum the components of the diamond.
23113
let n = 202300;
24114
let first = n * n * even_full;
25115
let second = (n + 1) * (n + 1) * odd_full;
@@ -38,6 +128,7 @@ pub fn part2(input: &Input) -> u64 {
38128
input.1
39129
}
40130

131+
/// Breadth first search from any number of starting locations with a limit on maximum steps.
41132
fn bfs(grid: &Grid<u8>, starts: &[Point], limit: u32) -> (u64, u64, u64, u64) {
42133
let mut grid = grid.clone();
43134
let mut todo = VecDeque::new();
@@ -53,6 +144,7 @@ fn bfs(grid: &Grid<u8>, starts: &[Point], limit: u32) -> (u64, u64, u64, u64) {
53144
}
54145

55146
while let Some((position, cost)) = todo.pop_front() {
147+
// First split by odd or even parity then by distance from the starting point.
56148
if cost % 2 == 1 {
57149
if position.manhattan(CENTER) <= 65 {
58150
odd_inner += 1;

0 commit comments

Comments
 (0)