aoc/year2022/day17.rs
1//! # Pyroclastic Flow
2//!
3//! ## Part One
4//!
5//! For speed we encode each rock shape as binary bits so that we can use bitwise logic to check
6//! for collisions. Each rock is encoded top to bottom and left to right. For example:
7//!
8//! ```none
9//! # 00010000 0x10
10//! ### => 00111000 => 0x38 => 0x00103810
11//! # 00010000 0x10
12//! ```
13//!
14//! The bits are shifted 2 away from the left wall. Walls are also encoded in binary, overlapping
15//! the left and right walls (no rock will ever collide first with a wall and its top row):
16//!
17//! ```none
18//! 100000001
19//! 100000001 => 0x01010101
20//! 100000001
21//! ```
22//!
23//! We store the accumulated tower efficiently as a vec of `u8` including the floor at index
24//! zero as a special pattern of `11111111`.
25//!
26//! We use bitwise AND to check for collisions between the rock, the walls and the existing tower
27//! in one operation.
28//!
29//! ## Part Two
30//!
31//! Since there's no reasonable way to analytically predict the height after some `n` rocks
32//! and brute force would take too long we can assume that there must be a
33//! [cycle](https://en.wikipedia.org/wiki/Cycle_(graph_theory)) in the output.
34//!
35//! We choose an arbitrary length and generate a sequence of that size then search
36//! for repeating patterns. Once we find the length of the cycle then we can extrapolate for
37//! any `n` greater than the start of the cycle.
38use std::iter::{Copied, Cycle};
39use std::slice::Iter;
40
41/// Convenience alias to shorten type name.
42type Wrapper<'a, T> = Cycle<Copied<Iter<'a, T>>>;
43
44/// Encode pieces one row per byte, highest row in the most significant position.
45const FLOOR: u8 = 0xff;
46const WALLS: u32 = 0x01010101;
47const ROCKS: [Rock; 5] = [
48 Rock { size: 1, shape: 0x0000003c },
49 Rock { size: 3, shape: 0x00103810 },
50 Rock { size: 3, shape: 0x00080838 },
51 Rock { size: 4, shape: 0x20202020 },
52 Rock { size: 2, shape: 0x00003030 },
53];
54
55#[derive(Copy, Clone)]
56struct Rock {
57 size: usize,
58 shape: u32,
59}
60
61struct State<'a> {
62 rocks: Wrapper<'a, Rock>,
63 jets: Wrapper<'a, u8>,
64 tower: Vec<u8>,
65 height: usize,
66}
67
68impl State<'_> {
69 fn new(input: &[u8]) -> State<'_> {
70 // Rocks and jets repeat endlessly.
71 // 13,000 is the the maximum possible height that the tower could reach after 5000 rocks.
72 let mut state = State {
73 rocks: ROCKS.iter().copied().cycle(),
74 jets: input.iter().copied().cycle(),
75 tower: vec![0; 13_000],
76 height: 0,
77 };
78 state.tower[0] = FLOOR;
79 state
80 }
81}
82
83/// Implement as an iterator for ergonomics.
84impl Iterator for State<'_> {
85 type Item = usize;
86
87 fn next(&mut self) -> Option<Self::Item> {
88 let Rock { size, mut shape } = self.rocks.next().unwrap();
89 let mut chunk = WALLS;
90 // Start 3 rows above the current top of the tower.
91 let mut index = self.height + 3;
92
93 loop {
94 let jet = self.jets.next().unwrap();
95 let candidate = if jet == b'<' { shape.rotate_left(1) } else { shape.rotate_right(1) };
96 // Check for a horizontal collision (this does not prevent downwards movement).
97 if candidate & chunk == 0 {
98 shape = candidate;
99 }
100
101 // The neat part of using bitwise AND to compare is that we can check all four
102 // rows in a single operation, including both walls and the existing tower.
103 chunk = (chunk << 8) | WALLS | (self.tower[index] as u32);
104
105 if shape & chunk == 0 {
106 // Keep falling
107 index -= 1;
108 } else {
109 // Add the new piece to the tower.
110 let bytes = shape.to_le_bytes();
111 self.tower[index + 1] |= bytes[0];
112 self.tower[index + 2] |= bytes[1];
113 self.tower[index + 3] |= bytes[2];
114 self.tower[index + 4] |= bytes[3];
115 // Rock may have fallen far enough to not add any additional height.
116 self.height = self.height.max(index + size);
117 break Some(self.height);
118 }
119 }
120 }
121}
122
123pub fn parse(input: &str) -> &[u8] {
124 input.trim().as_bytes()
125}
126
127pub fn part1(input: &[u8]) -> usize {
128 State::new(input).nth(2021).unwrap()
129}
130
131pub fn part2(input: &[u8]) -> usize {
132 // We make two complete [SWAGs](https://en.wikipedia.org/wiki/Scientific_wild-ass_guess):
133 // * 1000 row deltas are enough to form a unique prefix
134 // * The tower pattern will repeat in a cycle in first 5000 rows.
135 let guess = 1000;
136 let height: Vec<_> = State::new(input).take(5 * guess).collect();
137 // We compare based on the *delta* between rows instead of absolute heights.
138 let deltas: Vec<_> = height
139 .iter()
140 .scan(0, |state, &height| {
141 let delta = height - *state;
142 *state = height;
143 Some(delta)
144 })
145 .collect();
146
147 // Simple brute force check, instead of a
148 // [cycle detection](https://en.wikipedia.org/wiki/Cycle_detection) algorithm.
149 let end = deltas.len() - guess;
150 let needle = &deltas[end..];
151 let start = deltas.windows(guess).position(|w| w == needle).unwrap();
152
153 // Now that we know when the cycle repeats, we can work out the height for any arbitrary
154 // number of rocks after that point.
155 let cycle_height = height[end] - height[start];
156 let cycle_width = end - start;
157 let offset = 1_000_000_000_000 - 1 - start;
158 let quotient = offset / cycle_width;
159 let remainder = offset % cycle_width;
160 (quotient * cycle_height) + height[start + remainder]
161}