Skip to content

Commit fb78c1f

Browse files
committedJun 3, 2023
Year 2021 Day 18 documentation plus day 17 fixes.
1 parent f136588 commit fb78c1f

File tree

2 files changed

+75
-5
lines changed

2 files changed

+75
-5
lines changed
 

‎src/year2021/day17.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//! For example launching a probe at a y-velocity of 5 initially,
88
//! would result in a speed and y-position:
99
//!
10-
//! ```
10+
//! ```text
1111
//! Time: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
1212
//! Speed: 5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5, -6, -7
1313
//! Y-Position: 0, 5, 9, 12, 14, 15, 15, 14, 12, 9, 5, 0, -6
@@ -31,7 +31,8 @@
3131
//! counts how many are still in the target area at time `t`.
3232
//!
3333
//! For example using the sample `target area: x=20..30, y=-10..-5` gives a progression:
34-
//! ```
34+
//!
35+
//! ```text
3536
//! X-Velocity : 6
3637
//! Time: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
3738
//! New: 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
@@ -59,7 +60,8 @@
5960
//! this happens we add *both* `new` and `continuing` to the total. For subsequent times while we're
6061
//! still in the target area we add only the `new` values, as the `continuing` are trajectories
6162
//! that we've already considered. For example for an initial y-velocity of 0:
62-
//! ```
63+
//!
64+
//! ```text
6365
//! Time: 0, 1, 2, 3, 4
6466
//! Speed: 0, -1, -2, -3, -4
6567
//! Y-Position: 0, -1, -3, -6, -10

‎src/year2021/day18.rs

+70-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,48 @@
1+
//! # Snailfish
2+
//!
3+
//! The key observation is that snailfish numbers represent
4+
//! [binary trees](https://en.wikipedia.org/wiki/Binary_tree).
5+
//!
6+
//! For example the first four sample numbers on the problem description look like the following
7+
//! in binary tree form:
8+
//!
9+
//! ```text
10+
//! [1,2] [[1,2],3] [9,[8,7]] [[1,9],[8,5]]
11+
//! ■ ■ ■ ■
12+
//! / \ / \ / \ / \
13+
//! 1 2 ■ 3 9 ■ ■ ■
14+
//! / \ / \ / \ / \
15+
//! 1 2 8 7 1 9 8 5
16+
//! ```
17+
//!
18+
//! The addition rules have an important consequence. Exploding removes two leaf nodes at depth 5
19+
//! and moves them to neighbouring nodes. Since exploding repeatedly happens before splitting until
20+
//! there are no more values at depth 5 this means that the tree will never exceed a depth of 5.
21+
//!
22+
//! Each level of a tree can contain up to 2ⁿ nodes, so the maximum size of a snailfish tree is
23+
//! 1 + 2 + 4 + 8 + 16 + 32 = 2⁶-1 = 63 nodes.
24+
//!
25+
//! This means that we can store each snailfish number as an implicit data structure in a fixed size
26+
//! array. This is faster, smaller and more convenient than using a traditional struct with pointers.
27+
//! The root node is stored at index 0. For a node at index `i` its left child is at index
28+
//! `2i + 1`, right child at index `2i + 2` and parent at index `i / 2`. As leaf nodes are
29+
//! always greater than or equal to zero, `-1` is used as a special sentinel value for non-leaf nodes.
30+
131
type Snailfish = [i32; 63];
232

33+
/// The indices for [in-order traversal](https://en.wikipedia.org/wiki/Tree_traversal) of the first
34+
/// 4 levels of the implicit binary tree stored in an array.
335
const IN_ORDER: [usize; 30] = [
436
1, 3, 7, 15, 16, 8, 17, 18, 4, 9, 19, 20, 10, 21, 22, 2, 5, 11, 23, 24, 12, 25, 26, 6, 13, 27,
537
28, 14, 29, 30,
638
];
739

40+
/// Parse a snailfish number into an implicit binary tree stored in an array.
41+
///
42+
/// Since no number will greater than 9 initially we can consider each character individually.
43+
/// `[` means moves down a level to parse children, `,` means move from left to right node,
44+
/// `]` means move up a level to return to parent and a digit from 0-9 creates a leaf node
45+
/// with that value.
846
pub fn parse(input: &str) -> Vec<Snailfish> {
947
fn helper(bytes: &[u8]) -> Snailfish {
1048
let mut tree = [-1; 63];
@@ -24,11 +62,14 @@ pub fn parse(input: &str) -> Vec<Snailfish> {
2462
input.lines().map(|line| helper(line.as_bytes())).collect()
2563
}
2664

65+
/// Add all snailfish numbers, reducing to a single magnitude.
2766
pub fn part1(input: &[Snailfish]) -> i32 {
2867
let mut sum = input.iter().copied().reduce(|acc, n| add(&acc, &n)).unwrap();
2968
magnitude(&mut sum)
3069
}
3170

71+
/// Find the largest magnitude of any two snailfish numbers, remembering that snailfish addition
72+
/// is *not* commutative.
3273
pub fn part2(input: &[Snailfish]) -> i32 {
3374
let mut result = 0;
3475

@@ -43,6 +84,14 @@ pub fn part2(input: &[Snailfish]) -> i32 {
4384
result
4485
}
4586

87+
/// Add two snailfish numbers.
88+
///
89+
/// The initial step creates a new root node then makes the numbers the left and right children
90+
/// of this new root node, by copying the respective ranges of the implicit trees.
91+
///
92+
/// We can optimize the rules a little. This initial combination is the only time that more than one
93+
/// pair will be 4 levels deep simultaneously, so we can sweep from left to right on all possible
94+
/// leaf nodes in one pass.
4695
fn add(left: &Snailfish, right: &Snailfish) -> Snailfish {
4796
let mut tree = [-1; 63];
4897

@@ -66,10 +115,19 @@ fn add(left: &Snailfish, right: &Snailfish) -> Snailfish {
66115
tree
67116
}
68117

118+
/// Explode a specific pair identified by an index.
119+
///
120+
/// Storing the tree as an implicit structure has a nice benefit that finding the next left or right
121+
/// node is straightforward. We first move to the next left or right leaf node by adding or
122+
/// subtracting one to the index. If this node is empty then we move to the parent node until we
123+
/// find a leaf node.
124+
///
125+
/// The leaf node at index 31 has no possible nodes to the left and similarly the leaf node at
126+
/// index 62 has no possible nodes to the right.
69127
fn explode(tree: &mut Snailfish, pair: usize) {
70128
if pair > 31 {
71129
let mut i = pair - 1;
72-
while i > 0 {
130+
loop {
73131
if tree[i] >= 0 {
74132
tree[i] += tree[pair];
75133
break;
@@ -80,7 +138,7 @@ fn explode(tree: &mut Snailfish, pair: usize) {
80138

81139
if pair < 61 {
82140
let mut i = pair + 2;
83-
while i > 0 {
141+
loop {
84142
if tree[i] >= 0 {
85143
tree[i] += tree[pair + 1];
86144
break;
@@ -94,6 +152,12 @@ fn explode(tree: &mut Snailfish, pair: usize) {
94152
tree[(pair - 1) / 2] = 0;
95153
}
96154

155+
/// Split a node into two child nodes.
156+
///
157+
/// Search the tree in an *in-order* traversal, splitting the first node over `10` found (if any).
158+
/// We can optimize the rules by immediately exploding if this results in a node 4 levels deep,
159+
/// as we know that the prior optimzation in the [`add`] function means that this is the only
160+
/// explosion possible.
97161
fn split(tree: &mut Snailfish) -> bool {
98162
for &i in IN_ORDER.iter() {
99163
if tree[i] >= 10 {
@@ -110,6 +174,10 @@ fn split(tree: &mut Snailfish) -> bool {
110174
false
111175
}
112176

177+
/// Calculate the magnitude of a snailfish number in place without using recursion.
178+
///
179+
/// This operation is destructive but much faster than using a recursive approach and acceptable
180+
/// as we no longer need the original snailfish number afterwards.
113181
fn magnitude(tree: &mut Snailfish) -> i32 {
114182
for i in (0..31).rev() {
115183
if tree[i] == -1 {

0 commit comments

Comments
 (0)
Please sign in to comment.