aoc/year2023/
day22.rs

1//! # Sand Slabs
2//!
3//! Inspecting the input provides a useful insight. The x and y coordinates of bricks are
4//! restricted to between to 0 and 9 inclusive so the final shape of the pile will resemble a tall
5//! narrow tower similar to a [Jenga game](https://en.wikipedia.org/wiki/Jenga).
6//!
7//! A second insight is that this is a graph problem in disguise. Sorting the bricks in ascending
8//! z order is equivalent to a [topological sort](https://en.wikipedia.org/wiki/Topological_sorting)
9//! where each brick is a node and a directed edge links bricks that support other bricks.
10//!
11//! By iterating over each brick in order its final resting location and supporting bricks can be
12//! calculated immediately. For example taking the first 3 example bricks:
13//!
14//! ```none
15//!     Brick               Heights    Indices
16//!
17//!     1,0,1~1,2,1 <- A    0 1 0      X 0 X    Already in final position
18//!                         0 1 0      X 0 X
19//!                         0 1 0      X 0 X
20//!
21//!     0,0,2~2,0,2 <- B    2 2 2      1 1 1    Already in final position
22//!                         0 1 0      X 0 X
23//!                         0 1 0      X 0 X
24//!
25//!     0,2,3~2,2,3 <- C    2 2 2      1 1 1    Moves downwards by 1
26//!                         0 1 0      X 0 X
27//!                         2 2 2      2 2 2
28//! ```
29//!
30//! ## Part One
31//!
32//! If a brick is supported by only a single brick then the parent brick cannot be safely removed
33//! so we mark it as unsafe. Mutiple bricks could potentially be independently supported by a
34//! single parent brick so using a boolean flag means that we won't overcount.
35//!
36//! ## Part Two
37//!
38//! Unsafe bricks are a [dominator](https://en.wikipedia.org/wiki/Dominator_(graph_theory)) in
39//! graph theory as every path from the root (floor) to bricks supported by them must pass through
40//! the unsafe node.
41//!
42//! To count the total number of bricks that fall when all unsafe bricks are removed one at a time
43//! we build a linked list of bricks as we iterate through the nodes. Each brick has a `depth`
44//! which is the number of unsafe "dominator" nodes that connect it to the root. For example:
45//!
46//! ```none
47//!
48//!     Depth   0     1     2     1     0
49//!           | A ┬-> B --> C ┬-> D ┬-> E
50//!           |   |           |     |
51//!     Floor |   └-> F ------┘     |
52//!           | G ------------------┘
53//! ```
54//!
55//! * `A` and `G` rest on the floor so their depth is 0 as they can never fall.
56//! * `B` and `F` are both supported only by `A` so their depth is 1.
57//! * `C` will fall if either `A` or `B` is removed so its depth is 2.
58//! * `D` will only fall when `A` is removed. Removing `F` would leave it supported by `B` and `C`
59//!   or vice-versa. The common ancestor of the path to the root is `A` so its depth is 1.
60//! * `E`'s common ancestor is the floor so its depth is 0.
61//!
62//! In total `0 (A) + 0 (G) + 1 (B) + 1 (F) + 2 (C) + 1 (D) + 0 (E) = 5` bricks will fall.
63use crate::util::iter::*;
64use crate::util::parse::*;
65
66type Input = (usize, usize);
67
68pub fn parse(input: &str) -> Input {
69    // Parse each brick into an array of 6 elements, one for each coordinate.
70    let mut bricks: Vec<_> = input.iter_unsigned::<usize>().chunk::<6>().collect();
71    // x and y are limited to 10 in each direction so we can use a fixed size array.
72    let mut heights = [0; 100];
73    let mut indices = [usize::MAX; 100];
74
75    // Calculate the answer to both parts simultaneously for efficiency.
76    let mut safe = vec![true; bricks.len()];
77    let mut dominator: Vec<(usize, usize)> = Vec::with_capacity(bricks.len());
78
79    // Sort ascending by lowest z coordinate.
80    bricks.sort_unstable_by_key(|b| b[2]);
81
82    for (i, &[x1, y1, z1, x2, y2, z2]) in bricks.iter().enumerate() {
83        // Treat the 1D array as a 2D grid.
84        let start = 10 * y1 + x1;
85        let end = 10 * y2 + x2;
86        let step = if y2 > y1 { 10 } else { 1 };
87        let height = z2 - z1 + 1;
88
89        // Track what's underneath the brick.
90        let mut top = 0;
91        let mut previous = usize::MAX;
92        let mut underneath = 0;
93        let mut parent = 0;
94        let mut depth = 0;
95
96        // Find the highest z coordinate underneath the brick looking downwards along the z axis
97        // so only considering x and y coordinates.
98        for j in (start..=end).step_by(step) {
99            top = top.max(heights[j]);
100        }
101
102        for j in (start..=end).step_by(step) {
103            if heights[j] == top {
104                let index = indices[j];
105                if index != previous {
106                    previous = index;
107                    underneath += 1;
108
109                    if underneath == 1 {
110                        (parent, depth) = dominator[previous];
111                    } else {
112                        // Find common ancestor
113                        let (mut a, mut b) = (parent, depth);
114                        let (mut x, mut y) = dominator[previous];
115
116                        // The depth must be the same.
117                        while b > y {
118                            (a, b) = dominator[a];
119                        }
120                        while y > b {
121                            (x, y) = dominator[x];
122                        }
123
124                        // Bricks at the same depth could still have different paths from the
125                        // root so we need to also check the indices match.
126                        while a != x {
127                            (a, b) = dominator[a];
128                            (x, _) = dominator[x];
129                        }
130
131                        (parent, depth) = (a, b);
132                    }
133                }
134            }
135
136            // Update the x-y grid underneath the brick the with the new highest point and index.
137            heights[j] = top + height;
138            indices[j] = i;
139        }
140
141        // Increase depth by one for each dominator node in the path from the root.
142        if underneath == 1 {
143            safe[previous] = false;
144            parent = previous;
145            depth = dominator[previous].1 + 1;
146        }
147
148        dominator.push((parent, depth));
149    }
150
151    let part_one = safe.iter().filter(|&&b| b).count();
152    let part_two = dominator.iter().map(|(_, d)| d).sum();
153    (part_one, part_two)
154}
155
156pub fn part1(input: &Input) -> usize {
157    input.0
158}
159
160pub fn part2(input: &Input) -> usize {
161    input.1
162}