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, once};
39use std::slice::Iter;
40
41/// Encode pieces one row per byte, highest row in the most significant position.
42const FLOOR: u8 = 0xff;
43const WALLS: u32 = 0x01010101;
44const ROCKS: [Rock; 5] = [
45 Rock { size: 1, shape: 0x0000003c },
46 Rock { size: 3, shape: 0x00103810 },
47 Rock { size: 3, shape: 0x00080838 },
48 Rock { size: 4, shape: 0x20202020 },
49 Rock { size: 2, shape: 0x00003030 },
50];
51
52/// Convenience alias to shorten type name.
53type Wrapper<'a, T> = Cycle<Copied<Iter<'a, T>>>;
54
55#[derive(Clone, Copy)]
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 // 13,000 is the maximum possible height that the tower could reach after 5000 rocks.
71 let mut tower = vec![0; 13_000];
72 tower[0] = FLOOR;
73
74 // Rocks and jets repeat endlessly.
75 let rocks = ROCKS.iter().copied().cycle();
76 let jets = input.iter().copied().cycle();
77 State { rocks, jets, tower, height: 0 }
78 }
79}
80
81/// Implement as an iterator for ergonomics.
82impl Iterator for State<'_> {
83 type Item = usize;
84
85 fn next(&mut self) -> Option<Self::Item> {
86 let Rock { size, mut shape } = self.rocks.next().unwrap();
87 let mut chunk = WALLS;
88 // Start 3 rows above the current top of the tower.
89 let mut index = self.height + 3;
90
91 loop {
92 let jet = self.jets.next().unwrap();
93 let candidate = if jet == b'<' { shape.rotate_left(1) } else { shape.rotate_right(1) };
94 // Check for a horizontal collision (this does not prevent downwards movement).
95 if candidate & chunk == 0 {
96 shape = candidate;
97 }
98
99 // The neat part of using bitwise AND to compare is that we can check all four
100 // rows in a single operation, including both walls and the existing tower.
101 chunk = (chunk << 8) | WALLS | (self.tower[index] as u32);
102
103 if shape & chunk == 0 {
104 // Keep falling.
105 index -= 1;
106 } else {
107 // Add the new piece to the tower.
108 let bytes = shape.to_le_bytes();
109 self.tower[index + 1] |= bytes[0];
110 self.tower[index + 2] |= bytes[1];
111 self.tower[index + 3] |= bytes[2];
112 self.tower[index + 4] |= bytes[3];
113 // Rock may have fallen far enough to not add any additional height.
114 self.height = self.height.max(index + size);
115 break Some(self.height);
116 }
117 }
118 }
119}
120
121pub fn parse(input: &str) -> &[u8] {
122 input.trim().as_bytes()
123}
124
125pub fn part1(input: &[u8]) -> usize {
126 State::new(input).nth(2021).unwrap()
127}
128
129pub fn part2(input: &[u8]) -> usize {
130 // We make two complete [SWAGs](https://en.wikipedia.org/wiki/Scientific_wild-ass_guess):
131 // * 1000 row deltas are enough to form a unique prefix
132 // * The tower pattern will repeat in a cycle in the first 5000 rows.
133 let guess = 1000;
134 let height: Vec<_> = State::new(input).take(5 * guess).collect();
135 // We compare based on the *delta* between rows instead of absolute heights.
136 let deltas: Vec<_> =
137 once(height[0]).chain(height.array_windows().map(|[a, b]| b - a)).collect();
138
139 // Simple brute force check, instead of a
140 // [cycle detection](https://en.wikipedia.org/wiki/Cycle_detection) algorithm.
141 let end = deltas.len() - guess;
142 let needle = &deltas[end..];
143 let start = deltas.windows(guess).position(|w| w == needle).unwrap();
144
145 // Now that we know when the cycle repeats, we can work out the height for any arbitrary
146 // number of rocks after that point.
147 let cycle_height = height[end] - height[start];
148 let cycle_width = end - start;
149 let offset = 1_000_000_000_000 - 1 - start;
150 let quotient = offset / cycle_width;
151 let remainder = offset % cycle_width;
152 (quotient * cycle_height) + height[start + remainder]
153}