Skip to content

Commit d81427e

Browse files
committed
Document day 22
1 parent 590133f commit d81427e

File tree

1 file changed

+75
-0
lines changed

1 file changed

+75
-0
lines changed

src/year2023/day22.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,100 @@
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.
163
use crate::util::iter::*;
264
use crate::util::parse::*;
365

466
type Input = (usize, usize);
567

668
pub fn parse(input: &str) -> Input {
69+
// Parse each brick into an array of 6 elements, one for each coordinate.
770
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.
872
let mut heights = [0; 100];
973
let mut indices = [usize::MAX; 100];
1074

75+
// Calculate the answer to both parts simultaneously for efficiency.
1176
let mut safe = vec![true; bricks.len()];
1277
let mut dominator: Vec<(usize, usize)> = Vec::with_capacity(bricks.len());
1378

1479
// Sort ascending by lowest z coordinate.
1580
bricks.sort_unstable_by_key(|b| b[2]);
1681

1782
for (i, &[x1, y1, z1, x2, y2, z2]) in bricks.iter().enumerate() {
83+
// Treat the 1D array as a 2D grid.
1884
let start = 10 * y1 + x1;
1985
let end = 10 * y2 + x2;
2086
let step = if y2 > y1 { 10 } else { 1 };
2187
let height = z2 - z1 + 1;
2288

89+
// Track what's underneath the brick.
2390
let mut top = 0;
2491
let mut previous = usize::MAX;
2592
let mut underneath = 0;
2693
let mut parent = 0;
2794
let mut depth = 0;
2895

96+
// Find the highest z coordinate underneath the brick looking downwards along the z axis
97+
// so only considering x and y coordinates.
2998
for j in (start..=end).step_by(step) {
3099
top = top.max(heights[j]);
31100
}
@@ -44,12 +113,16 @@ pub fn parse(input: &str) -> Input {
44113
let (mut a, mut b) = (parent, depth);
45114
let (mut x, mut y) = dominator[previous];
46115

116+
// The depth must be the same.
47117
while b > y {
48118
(a, b) = dominator[a];
49119
}
50120
while y > b {
51121
(x, y) = dominator[x];
52122
}
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.
53126
while a != x {
54127
(a, b) = dominator[a];
55128
(x, _) = dominator[x];
@@ -60,10 +133,12 @@ pub fn parse(input: &str) -> Input {
60133
}
61134
}
62135

136+
// Update the x-y grid underneath the brick the with the new highest point and index.
63137
heights[j] = top + height;
64138
indices[j] = i;
65139
}
66140

141+
// Increase depth by one for each dominator node in the path from the root.
67142
if underneath == 1 {
68143
safe[previous] = false;
69144
parent = previous;

0 commit comments

Comments
 (0)