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