aoc/year2023/day14.rs
1//! # Parabolic Reflector Dish
2//!
3//! To solve part two we look for a cycle where the dish returns to a previously seen state.
4//! By storing each dish and a index in a `HashMap` we can calculate the offset and length of the
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//! ```
67use crate::util::grid::*;
68use crate::util::hash::*;
69use crate::util::point::*;
70
71pub struct Input {
72 width: i32,
73 height: i32,
74 // Index of each ball.
75 rounded: Vec<i16>,
76 // Index into corresponding `roll_` vec for each possible grid location.
77 fixed_north: Vec<i16>,
78 fixed_west: Vec<i16>,
79 fixed_south: Vec<i16>,
80 fixed_east: Vec<i16>,
81 // The current index of the ball resting against each fixed point.
82 roll_north: Vec<i16>,
83 roll_west: Vec<i16>,
84 roll_south: Vec<i16>,
85 roll_east: Vec<i16>,
86}
87
88pub fn parse(input: &str) -> Input {
89 // Expand the grid by 2 in each direction to handle edges the same way as fixed points.
90 let inner = Grid::parse(input);
91 let mut grid = Grid::new(inner.width + 2, inner.height + 2, b'#');
92
93 // Copy inner grid.
94 for y in 0..inner.width {
95 for x in 0..inner.width {
96 let src = Point::new(x, y);
97 let dst = Point::new(x + 1, y + 1);
98 grid[dst] = inner[src];
99 }
100 }
101
102 let mut rounded = Vec::new();
103 let mut north = grid.same_size_with(0);
104 let mut west = grid.same_size_with(0);
105 let mut south = grid.same_size_with(0);
106 let mut east = grid.same_size_with(0);
107 let mut roll_north = Vec::new();
108 let mut roll_west = Vec::new();
109 let mut roll_south = Vec::new();
110 let mut roll_east = Vec::new();
111
112 // Starting index of each rounded ball.
113 for y in 0..grid.height {
114 for x in 0..grid.width {
115 let point = Point::new(x, y);
116 if grid[point] == b'O' {
117 rounded.push((grid.width * point.y + point.x) as i16);
118 }
119 }
120 }
121
122 // For each direction, store the next index that a ball will roll to in that direction.
123
124 // North
125 for x in 0..grid.width {
126 for y in 0..grid.height {
127 let point = Point::new(x, y);
128 if grid[point] == b'#' {
129 roll_north.push((grid.width * point.y + point.x) as i16);
130 }
131 north[point] = (roll_north.len() - 1) as i16;
132 }
133 }
134
135 // West
136 for y in 0..grid.height {
137 for x in 0..grid.width {
138 let point = Point::new(x, y);
139 if grid[point] == b'#' {
140 roll_west.push((grid.width * point.y + point.x) as i16);
141 }
142 west[point] = (roll_west.len() - 1) as i16;
143 }
144 }
145
146 // South
147 for x in 0..grid.width {
148 for y in (0..grid.height).rev() {
149 let point = Point::new(x, y);
150 if grid[point] == b'#' {
151 roll_south.push((grid.width * point.y + point.x) as i16);
152 }
153 south[point] = (roll_south.len() - 1) as i16;
154 }
155 }
156
157 // East
158 for y in 0..grid.height {
159 for x in (0..grid.width).rev() {
160 let point = Point::new(x, y);
161 if grid[point] == b'#' {
162 roll_east.push((grid.width * point.y + point.x) as i16);
163 }
164 east[point] = (roll_east.len() - 1) as i16;
165 }
166 }
167
168 Input {
169 width: grid.width,
170 height: grid.height,
171 rounded,
172 fixed_north: north.bytes,
173 fixed_west: west.bytes,
174 fixed_south: south.bytes,
175 fixed_east: east.bytes,
176 roll_north,
177 roll_west,
178 roll_south,
179 roll_east,
180 }
181}
182
183pub fn part1(input: &Input) -> i32 {
184 let Input { width, height, fixed_north, roll_north, .. } = input;
185
186 // Tilt north only once.
187 let mut result = 0;
188 let rounded = &mut input.rounded.clone();
189 let state = tilt(rounded, fixed_north, roll_north, *width as i16);
190
191 // Find vertical distance of each ball from the bottom, remembering that the grid is 2 bigger.
192 for (&a, &b) in input.roll_north.iter().zip(state.iter()) {
193 for index in (a..b).step_by(input.width as usize) {
194 let y = (index as i32) / width;
195 result += height - 2 - y;
196 }
197 }
198
199 result
200}
201
202pub fn part2(input: &Input) -> i32 {
203 let Input { width, height, .. } = input;
204
205 let rounded = &mut input.rounded.clone();
206 let mut seen = FastMap::with_capacity(100);
207
208 // Simulate tilting until a cycle is found.
209 let (start, end) = loop {
210 tilt(rounded, &input.fixed_north, &input.roll_north, *width as i16);
211 tilt(rounded, &input.fixed_west, &input.roll_west, 1);
212 tilt(rounded, &input.fixed_south, &input.roll_south, -(*width) as i16);
213 let state = tilt(rounded, &input.fixed_east, &input.roll_east, -1);
214
215 if let Some(previous) = seen.insert(state, seen.len()) {
216 break (previous, seen.len());
217 }
218 };
219
220 // Find the index of the state after 1 billion repetitions.
221 let offset = 1_000_000_000 - 1 - start;
222 let cycle_width = end - start;
223 let remainder = offset % cycle_width;
224 let target = start + remainder;
225
226 let (state, _) = seen.iter().find(|&(_, &i)| i == target).unwrap();
227 let mut result = 0;
228
229 for (&a, &b) in input.roll_east.iter().zip(state.iter()) {
230 // Number of balls resting against the fixed point.
231 let n = (a - b) as i32;
232 // Distance from bottom.
233 let y = (a as i32) / width;
234 // Total load.
235 result += n * (height - 1 - y);
236 }
237
238 result
239}
240
241/// Very fast calculation of new state after tilting in the specified direction.
242fn tilt(rounded: &mut [i16], fixed: &[i16], roll: &[i16], direction: i16) -> Vec<i16> {
243 let mut state = roll.to_vec();
244
245 for rock in rounded {
246 let index = fixed[*rock as usize] as usize;
247 state[index] += direction;
248 *rock = state[index];
249 }
250
251 state
252}