aoc/year2022/day14.rs
1//! # Regolith Reservoir
2//!
3//! We could simulate each grain of sand falling one at a time, for example:
4//!
5//! ```none
6//! #o# # # # # # #
7//! # # #o# # # # #
8//! # # => # # => #o# => # #
9//! # # # # # # #o#
10//! ### ### ### ###
11//! ```
12//!
13//! In this example it would take 4 steps to simulate the first grain, 3 steps for the second
14//! and so on. More generally it would take `∑n` = `n * (n + 1) / 2` or `O(n²)` complexity for
15//! the whole pile.
16//!
17//! We instead simulate in `O(n)` complexity by recursively check each grain's underneath
18//! neighbors until we have a conclusive result then propagating that back up the call stack,
19//! for example:
20//!
21//! ```none
22//! #?# #?# #?# #?# #?# #?# #?# #o#
23//! # # #?# #?# #?# #?# #?# #o# #o#
24//! # # => # # => #?# => #?# => #?# => #o# => #o# => #o#
25//! # # # # # # #?# #o# #o# #o# #o#
26//! ### ### ### ### ### ### ### ###
27//! ```
28//!
29//! We model the cave as a grid in the possible states:
30//! * `Air` Empty blocks, treated as unknown status when checking underneath neighbors.
31//! * `Falling` Grains of sand that will continue to fall continously forever.
32//! * `Stopped` Both original rock walls and any grains of sand that have come to rest.
33use crate::util::parse::*;
34
35#[derive(Clone, Copy, PartialEq, Eq)]
36enum Kind {
37 Air,
38 Falling,
39 Stopped,
40}
41
42#[derive(Clone)]
43pub struct Cave {
44 width: usize,
45 height: usize,
46 size: usize,
47 kind: Vec<Kind>,
48 floor: Kind,
49 count: u32,
50}
51
52impl Cave {
53 fn fall(&mut self, index: usize) -> Kind {
54 // Check in order: center, left then right
55 let result = self.check(index + self.width)
56 && self.check(index + self.width - 1)
57 && self.check(index + self.width + 1);
58
59 // If all 3 bottom neighbors are stopped then so are we.
60 // Cache the result into the grid then propagate result back up the call stack.
61 if result {
62 self.count += 1;
63 self.kind[index] = Kind::Stopped;
64 Kind::Stopped
65 } else {
66 self.kind[index] = Kind::Falling;
67 Kind::Falling
68 }
69 }
70
71 // Returns `true` if cell is stopped.
72 fn check(&mut self, index: usize) -> bool {
73 let kind = if index >= self.size {
74 // If we've reached the "floor" then return that.
75 self.floor
76 } else if self.kind[index] == Kind::Air {
77 // If we're unknown then recursively check our own underneath neighbors
78 self.fall(index)
79 } else {
80 // Otherwise use the cached value.
81 self.kind[index]
82 };
83 kind == Kind::Stopped
84 }
85}
86
87/// Creates a 2D grid cave exactly the maximum possible size.
88pub fn parse(input: &str) -> Cave {
89 let unsigned = |line: &str| line.iter_unsigned().collect();
90 let points: Vec<Vec<usize>> = input.lines().map(unsigned).collect();
91 let max_y = points.iter().flat_map(|row| row.iter().skip(1).step_by(2)).max().unwrap();
92 // Floor is 2 below the bottommost wall.
93 let height = max_y + 2;
94 // Allow enough horizontal room to spread out.
95 let width = 2 * height + 1;
96 let size = width * height;
97 let mut kind = vec![Kind::Air; size];
98
99 // Draw each of the walls.
100 for row in points {
101 for window in row.windows(4).step_by(2) {
102 if let &[x1, y1, x2, y2] = window {
103 for x in x1.min(x2)..=x1.max(x2) {
104 for y in y1.min(y2)..=y1.max(y2) {
105 kind[(width * y) + (x + height - 500)] = Kind::Stopped;
106 }
107 }
108 }
109 }
110 }
111
112 Cave { width, height, size, kind, floor: Kind::Air, count: 0 }
113}
114
115/// If a grain of sand reaches the floor it will fall forever.
116pub fn part1(input: &Cave) -> u32 {
117 simulate(input, Kind::Falling)
118}
119
120/// The floor is solid rock.
121pub fn part2(input: &Cave) -> u32 {
122 simulate(input, Kind::Stopped)
123}
124
125fn simulate(input: &Cave, floor: Kind) -> u32 {
126 let mut cave = input.clone();
127 cave.floor = floor;
128 // Height is also the x coordinate of the central starting location for grains.
129 cave.fall(cave.height);
130 cave.count
131}