Skip to content

Commit 45edebd

Browse files
committed
Document day 14
1 parent 23f52f8 commit 45edebd

File tree

1 file changed

+77
-3
lines changed

1 file changed

+77
-3
lines changed

src/year2023/day14.rs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,98 @@
33
//! To solve part two we look for a cycle where the dish returns to a previously seen state.
44
//! By storing each dish and a index in a `HashMap` we can calculate the offset and length of the
55
//! cycle then use that to find to state at the billionth step.
6+
//!
7+
//! Calculating the state needs to be done sequentially so we use some tricks to make it as fast as
8+
//! possible.
9+
//!
10+
//! First the location of each each ball is stored in a `vec`. My input had ~2,000 balls compared to
11+
//! 10,000 grid squares total, so this approach reduces the amount of data to scan by 5x. The 2D
12+
//! coordinates are converted so a 1D number, for example the index of a ball on the second row
13+
//! second column would be 1 * 100 + 1 = 101.
14+
//!
15+
//! Next for each possible tilt orientation (north, south, east and west) an approach similar to a
16+
//! prefix sum is used. Each edge or fixed rock is assigned an index. We expand the grid by 2 in
17+
//! each direction (one for each edge) to handles the edges. For example, using west (left):
18+
//!
19+
//! ```none
20+
//! ..#.#..
21+
//! ```
22+
//!
23+
//! is represented in `fixed_west` as (noticing the extra 0 for the left edge)
24+
//!
25+
//! ```none
26+
//! 0 0 0 1 1 2 2 2
27+
//! ```
28+
//!
29+
//! The the number of balls the come to rest against each fixed point is counted, for example:
30+
//!
31+
//! ```none
32+
//! OO#.#OO
33+
//! ```
34+
//!
35+
//! is stored in `roll_west` similar to:
36+
//!
37+
//! ```none
38+
//! 2 0 2
39+
//! ```
40+
//!
41+
//! This approach has two huge advantages:
42+
//!
43+
//! First, the number of balls resting against each fixed point completely represents the state of the
44+
//! grid in a very compact format. For example my input has ~1600 fixed points. Using 2 bytes per
45+
//! point needs 3.2K total to represent the grid, compared to 100 * 100 = 10K for the simple approach.
46+
//! 3x less data is 3x faster to hash when storing states in a `HashMap` looking for duplicates.
47+
//!
48+
//! Second, calculating the new position of a ball is very fast. For each ball:
49+
//!
50+
//! * Use `fixed_*` to lookup the index in the corresponding `roll_*` vec.
51+
//! * This stores the current index of the last ball resting against that fixed point.
52+
//! * Increment this value by ±1 for horizontal movement or ±width for vertical movement
53+
//! and then update the new location of this ball.
54+
//!
55+
//! For example, tilting a single row west, processing each ball from left to right where each line
56+
//! represent the new state would look like:
57+
//!
58+
//! ```none
59+
//! grid rounded fixed_west roll_west
60+
//! .O#..O.OO.#..O [1 5 7 8 13] [0 0 1 1 1 1 1 1 1 1 2 2 2 2] [-1 2 10]
61+
//! O.#..O.OO.#..O [0 5 7 8 13] [0 0 1 1 1 1 1 1 1 1 2 2 2 2] [0 2 10]
62+
//! O.#O...OO.#..O [0 3 7 8 13] [0 0 1 1 1 1 1 1 1 1 2 2 2 2] [0 3 10]
63+
//! O.#OO...O.#..O [0 3 4 8 13] [0 0 1 1 1 1 1 1 1 1 2 2 2 2] [0 4 10]
64+
//! O.#OOO....#..O [0 3 4 5 13] [0 0 1 1 1 1 1 1 1 1 2 2 2 2] [0 5 10]
65+
//! O.#OOO....#O.. [0 3 4 5 11] [0 0 1 1 1 1 1 1 1 1 2 2 2 2] [0 5 11]
66+
//! ```
667
use crate::util::grid::*;
768
use crate::util::hash::*;
869
use crate::util::point::*;
970

1071
pub struct Input {
1172
width: i32,
1273
height: i32,
74+
// Index of each ball.
1375
rounded: Vec<i16>,
76+
// Index into corresponding `roll_` vec for each possible grid location.
1477
fixed_north: Vec<i16>,
1578
fixed_west: Vec<i16>,
1679
fixed_south: Vec<i16>,
1780
fixed_east: Vec<i16>,
81+
// The current index of the ball resting against each fixed point.
1882
roll_north: Vec<i16>,
1983
roll_west: Vec<i16>,
2084
roll_south: Vec<i16>,
2185
roll_east: Vec<i16>,
2286
}
2387

2488
pub fn parse(input: &str) -> Input {
89+
// Expand the grid by 2 in each direction to handle edges the same way as fixed points.
2590
let inner = Grid::parse(input);
2691
let mut grid = Grid {
2792
width: inner.width + 2,
2893
height: inner.height + 2,
2994
bytes: vec![b'#'; ((inner.width + 2) * (inner.height + 2)) as usize],
3095
};
3196

32-
// Copy
97+
// Copy inner grid.
3398
for y in 0..inner.width {
3499
for x in 0..inner.width {
35100
let src = Point::new(x, y);
@@ -50,7 +115,7 @@ pub fn parse(input: &str) -> Input {
50115
let mut roll_south = Vec::new();
51116
let mut roll_east = Vec::new();
52117

53-
// Rounded
118+
// Starting index of each rounded ball.
54119
for y in 0..grid.height {
55120
for x in 0..grid.width {
56121
let point = Point::new(x, y);
@@ -60,6 +125,8 @@ pub fn parse(input: &str) -> Input {
60125
}
61126
}
62127

128+
// For each direction, store the next index that a ball will roll to in that direction.
129+
63130
// North
64131
for x in 0..grid.width {
65132
for y in 0..grid.height {
@@ -122,10 +189,12 @@ pub fn parse(input: &str) -> Input {
122189
pub fn part1(input: &Input) -> i32 {
123190
let Input { width, height, fixed_north, roll_north, .. } = input;
124191

192+
// Tilt north only once.
125193
let mut result = 0;
126194
let rounded = &mut input.rounded.clone();
127195
let state = tilt(rounded, fixed_north, roll_north, *width as i16);
128196

197+
// Find vertical distance of each ball from the bottom, remembering that the grid is 2 bigger.
129198
for (&a, &b) in input.roll_north.iter().zip(state.iter()) {
130199
for index in (a..b).step_by(input.width as usize) {
131200
let y = (index as i32) / width;
@@ -142,7 +211,7 @@ pub fn part2(input: &Input) -> i32 {
142211
let rounded = &mut input.rounded.clone();
143212
let mut seen = FastMap::with_capacity(100);
144213

145-
// Find cycle
214+
// Simulate tilting until a cycle is found.
146215
let (start, end) = loop {
147216
tilt(rounded, &input.fixed_north, &input.roll_north, *width as i16);
148217
tilt(rounded, &input.fixed_west, &input.roll_west, 1);
@@ -154,6 +223,7 @@ pub fn part2(input: &Input) -> i32 {
154223
}
155224
};
156225

226+
// Find the index of the state after 1 billion repetitions.
157227
let offset = 1_000_000_000 - 1 - start;
158228
let cycle_width = end - start;
159229
let remainder = offset % cycle_width;
@@ -163,14 +233,18 @@ pub fn part2(input: &Input) -> i32 {
163233
let mut result = 0;
164234

165235
for (&a, &b) in input.roll_east.iter().zip(state.iter()) {
236+
// Number of balls resting against the fixed point.
166237
let n = (a - b) as i32;
238+
// Distance from bottom.
167239
let y = (a as i32) / width;
240+
// Total load.
168241
result += n * (height - 1 - y);
169242
}
170243

171244
result
172245
}
173246

247+
/// Very fast calculation of new state after tilting in the specified direction.
174248
fn tilt(rounded: &mut [i16], fixed: &[i16], roll: &[i16], direction: i16) -> Vec<i16> {
175249
let mut state = roll.to_vec();
176250

0 commit comments

Comments
 (0)