3
3
//! To solve part two we look for a cycle where the dish returns to a previously seen state.
4
4
//! By storing each dish and a index in a `HashMap` we can calculate the offset and length of the
5
5
//! 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
+ //! ```
6
67
use crate :: util:: grid:: * ;
7
68
use crate :: util:: hash:: * ;
8
69
use crate :: util:: point:: * ;
9
70
10
71
pub struct Input {
11
72
width : i32 ,
12
73
height : i32 ,
74
+ // Index of each ball.
13
75
rounded : Vec < i16 > ,
76
+ // Index into corresponding `roll_` vec for each possible grid location.
14
77
fixed_north : Vec < i16 > ,
15
78
fixed_west : Vec < i16 > ,
16
79
fixed_south : Vec < i16 > ,
17
80
fixed_east : Vec < i16 > ,
81
+ // The current index of the ball resting against each fixed point.
18
82
roll_north : Vec < i16 > ,
19
83
roll_west : Vec < i16 > ,
20
84
roll_south : Vec < i16 > ,
21
85
roll_east : Vec < i16 > ,
22
86
}
23
87
24
88
pub fn parse ( input : & str ) -> Input {
89
+ // Expand the grid by 2 in each direction to handle edges the same way as fixed points.
25
90
let inner = Grid :: parse ( input) ;
26
91
let mut grid = Grid {
27
92
width : inner. width + 2 ,
28
93
height : inner. height + 2 ,
29
94
bytes : vec ! [ b'#' ; ( ( inner. width + 2 ) * ( inner. height + 2 ) ) as usize ] ,
30
95
} ;
31
96
32
- // Copy
97
+ // Copy inner grid.
33
98
for y in 0 ..inner. width {
34
99
for x in 0 ..inner. width {
35
100
let src = Point :: new ( x, y) ;
@@ -50,7 +115,7 @@ pub fn parse(input: &str) -> Input {
50
115
let mut roll_south = Vec :: new ( ) ;
51
116
let mut roll_east = Vec :: new ( ) ;
52
117
53
- // Rounded
118
+ // Starting index of each rounded ball.
54
119
for y in 0 ..grid. height {
55
120
for x in 0 ..grid. width {
56
121
let point = Point :: new ( x, y) ;
@@ -60,6 +125,8 @@ pub fn parse(input: &str) -> Input {
60
125
}
61
126
}
62
127
128
+ // For each direction, store the next index that a ball will roll to in that direction.
129
+
63
130
// North
64
131
for x in 0 ..grid. width {
65
132
for y in 0 ..grid. height {
@@ -122,10 +189,12 @@ pub fn parse(input: &str) -> Input {
122
189
pub fn part1 ( input : & Input ) -> i32 {
123
190
let Input { width, height, fixed_north, roll_north, .. } = input;
124
191
192
+ // Tilt north only once.
125
193
let mut result = 0 ;
126
194
let rounded = & mut input. rounded . clone ( ) ;
127
195
let state = tilt ( rounded, fixed_north, roll_north, * width as i16 ) ;
128
196
197
+ // Find vertical distance of each ball from the bottom, remembering that the grid is 2 bigger.
129
198
for ( & a, & b) in input. roll_north . iter ( ) . zip ( state. iter ( ) ) {
130
199
for index in ( a..b) . step_by ( input. width as usize ) {
131
200
let y = ( index as i32 ) / width;
@@ -142,7 +211,7 @@ pub fn part2(input: &Input) -> i32 {
142
211
let rounded = & mut input. rounded . clone ( ) ;
143
212
let mut seen = FastMap :: with_capacity ( 100 ) ;
144
213
145
- // Find cycle
214
+ // Simulate tilting until a cycle is found.
146
215
let ( start, end) = loop {
147
216
tilt ( rounded, & input. fixed_north , & input. roll_north , * width as i16 ) ;
148
217
tilt ( rounded, & input. fixed_west , & input. roll_west , 1 ) ;
@@ -154,6 +223,7 @@ pub fn part2(input: &Input) -> i32 {
154
223
}
155
224
} ;
156
225
226
+ // Find the index of the state after 1 billion repetitions.
157
227
let offset = 1_000_000_000 - 1 - start;
158
228
let cycle_width = end - start;
159
229
let remainder = offset % cycle_width;
@@ -163,14 +233,18 @@ pub fn part2(input: &Input) -> i32 {
163
233
let mut result = 0 ;
164
234
165
235
for ( & a, & b) in input. roll_east . iter ( ) . zip ( state. iter ( ) ) {
236
+ // Number of balls resting against the fixed point.
166
237
let n = ( a - b) as i32 ;
238
+ // Distance from bottom.
167
239
let y = ( a as i32 ) / width;
240
+ // Total load.
168
241
result += n * ( height - 1 - y) ;
169
242
}
170
243
171
244
result
172
245
}
173
246
247
+ /// Very fast calculation of new state after tilting in the specified direction.
174
248
fn tilt ( rounded : & mut [ i16 ] , fixed : & [ i16 ] , roll : & [ i16 ] , direction : i16 ) -> Vec < i16 > {
175
249
let mut state = roll. to_vec ( ) ;
176
250
0 commit comments