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
+
1
31
type Snailfish = [ i32 ; 63 ] ;
2
32
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.
3
35
const IN_ORDER : [ usize ; 30 ] = [
4
36
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 ,
5
37
28 , 14 , 29 , 30 ,
6
38
] ;
7
39
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.
8
46
pub fn parse ( input : & str ) -> Vec < Snailfish > {
9
47
fn helper ( bytes : & [ u8 ] ) -> Snailfish {
10
48
let mut tree = [ -1 ; 63 ] ;
@@ -24,11 +62,14 @@ pub fn parse(input: &str) -> Vec<Snailfish> {
24
62
input. lines ( ) . map ( |line| helper ( line. as_bytes ( ) ) ) . collect ( )
25
63
}
26
64
65
+ /// Add all snailfish numbers, reducing to a single magnitude.
27
66
pub fn part1 ( input : & [ Snailfish ] ) -> i32 {
28
67
let mut sum = input. iter ( ) . copied ( ) . reduce ( |acc, n| add ( & acc, & n) ) . unwrap ( ) ;
29
68
magnitude ( & mut sum)
30
69
}
31
70
71
+ /// Find the largest magnitude of any two snailfish numbers, remembering that snailfish addition
72
+ /// is *not* commutative.
32
73
pub fn part2 ( input : & [ Snailfish ] ) -> i32 {
33
74
let mut result = 0 ;
34
75
@@ -43,6 +84,14 @@ pub fn part2(input: &[Snailfish]) -> i32 {
43
84
result
44
85
}
45
86
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.
46
95
fn add ( left : & Snailfish , right : & Snailfish ) -> Snailfish {
47
96
let mut tree = [ -1 ; 63 ] ;
48
97
@@ -66,10 +115,19 @@ fn add(left: &Snailfish, right: &Snailfish) -> Snailfish {
66
115
tree
67
116
}
68
117
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.
69
127
fn explode ( tree : & mut Snailfish , pair : usize ) {
70
128
if pair > 31 {
71
129
let mut i = pair - 1 ;
72
- while i > 0 {
130
+ loop {
73
131
if tree[ i] >= 0 {
74
132
tree[ i] += tree[ pair] ;
75
133
break ;
@@ -80,7 +138,7 @@ fn explode(tree: &mut Snailfish, pair: usize) {
80
138
81
139
if pair < 61 {
82
140
let mut i = pair + 2 ;
83
- while i > 0 {
141
+ loop {
84
142
if tree[ i] >= 0 {
85
143
tree[ i] += tree[ pair + 1 ] ;
86
144
break ;
@@ -94,6 +152,12 @@ fn explode(tree: &mut Snailfish, pair: usize) {
94
152
tree[ ( pair - 1 ) / 2 ] = 0 ;
95
153
}
96
154
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.
97
161
fn split ( tree : & mut Snailfish ) -> bool {
98
162
for & i in IN_ORDER . iter ( ) {
99
163
if tree[ i] >= 10 {
@@ -110,6 +174,10 @@ fn split(tree: &mut Snailfish) -> bool {
110
174
false
111
175
}
112
176
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.
113
181
fn magnitude ( tree : & mut Snailfish ) -> i32 {
114
182
for i in ( 0 ..31 ) . rev ( ) {
115
183
if tree[ i] == -1 {
0 commit comments