aoc/year2021/day18.rs
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 neighboring 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//! and even then a depth of 5 is transient.
22//!
23//! Each level of a tree can contain up to 2ⁿ nodes, so the maximum size of a non-transient
24//! snailfish tree is 1 + 2 + 4 + 8 + 16 = 2⁵ - 1 = 31 nodes.
25//!
26//! This means that we can store each snailfish number as an implicit data structure in a fixed-size
27//! array. This is faster, smaller and more convenient than using a traditional struct with
28//! pointers. The root node is stored at index 1 (index 0 is unused). For a node at index `i` its
29//! left child is at index `2i`, right child at index `2i + 1` and parent at index `i / 2`. As leaf
30//! nodes are always greater than or equal to zero, `-1` is used as a special sentinel value for
31//! non-leaf nodes.
32//!
33//! Another optimization is realizing that all of the explode actions before the first split can
34//! be pre-computed. Instead of passing two depth-4 numbers to `add()`, we can simplify any depth-4
35//! number into depth-3 via explode actions, and track what values it would have spilled left or
36//! right had it been part of a larger `add()`. The resulting `Compressed` object is then ready to
37//! slide into the left or right half of a new depth-4 tree at the start of `add()`, and all further
38//! reduce actions on the sum will be just splits of leaf nodes larger than 9, followed by an
39//! explode if the split happened at depth 4.
40use crate::util::parse::*;
41use crate::util::thread::*;
42
43type Snailfish = [i32; 32];
44
45pub struct Compressed {
46 left_spill: i32,
47 right_spill: i32,
48 nodes: [i32; 14],
49}
50
51/// Parse a snailfish number into an implicit binary tree stored in an array.
52///
53/// Since no number will be greater than 9 initially we can consider each character individually.
54/// `[` means move down a level to parse children, `,` means move from left to right node,
55/// `]` means move up a level to return to parent and a digit from 0-9 creates a leaf node
56/// with that value.
57pub fn parse(input: &str) -> Vec<Compressed> {
58 input
59 .lines()
60 .map(|line| {
61 let mut tree = [-1; 32];
62 let mut i = 1;
63
64 for b in line.bytes() {
65 match b {
66 b'[' => i *= 2,
67 b',' => i += 1,
68 b']' => i /= 2,
69 b => tree[i] = b.to_decimal(),
70 }
71 }
72
73 compress(tree)
74 })
75 .collect()
76}
77
78/// Add all snailfish numbers, reducing to a single magnitude.
79pub fn part1(input: &[Compressed]) -> i32 {
80 let first = add(&input[0], &input[1]);
81 let sum = input[2..].iter().fold(first, |sum, next| add(&compress(sum), next));
82 magnitude(sum)
83}
84
85/// Find the largest magnitude of any two snailfish numbers, remembering that snailfish addition
86/// is *not* commutative.
87pub fn part2(input: &[Compressed]) -> i32 {
88 // Use as many cores as possible to parallelize the calculation.
89 let result = spawn_parallel_iterator(input, |iter| {
90 iter.flat_map(|a| {
91 // Avoid pairing `a` with itself.
92 let index = input.element_offset(a).unwrap();
93 input[..index].iter().chain(&input[index + 1..]).map(|b| magnitude(add(a, b)))
94 })
95 .max()
96 });
97
98 result.into_iter().flatten().max().unwrap()
99}
100
101/// Add two snailfish numbers.
102///
103/// The initial step creates a new root node then makes the numbers the left and right children
104/// of this new root node, by copying the respective ranges of the implicit trees.
105///
106/// We can optimize the rules a little; the first round of explode was already done in creating
107/// compressed arguments, and a split runs its own inline explode.
108fn add(left: &Compressed, right: &Compressed) -> Snailfish {
109 let mut tree = [-1; 32];
110
111 // Copy left into place.
112 tree[4..6].copy_from_slice(&left.nodes[0..2]);
113 tree[8..12].copy_from_slice(&left.nodes[2..6]);
114 tree[16..24].copy_from_slice(&left.nodes[6..14]);
115
116 // Copy right into place.
117 tree[6..8].copy_from_slice(&right.nodes[0..2]);
118 tree[12..16].copy_from_slice(&right.nodes[2..6]);
119 tree[24..32].copy_from_slice(&right.nodes[6..14]);
120
121 // Adjust by the explode spillover between sides.
122 match (right.left_spill, left.right_spill) {
123 (-1, -1) => (),
124 (left_spill, -1) => augment_leaf(&mut tree, left_spill, 23),
125 (-1, right_spill) => augment_leaf(&mut tree, right_spill, 24),
126 (left_spill, right_spill) => tree[23] += left_spill + right_spill,
127 }
128
129 // Now we process all split operations; any further explode actions are done during any split
130 // that creates a temporary depth 5.
131 split(&mut tree);
132 tree
133}
134
135/// Perform all initial explodes to create a compressed number from a snailfish number.
136/// This is a destructive operation, as no caller needs the original afterwards.
137fn compress(mut tree: Snailfish) -> Compressed {
138 for from in 17..31 {
139 let to = if from % 2 == 0 { from / 2 - 1 } else { from + 1 };
140 let value = tree[from];
141 if value >= 0 {
142 tree[from / 2] = 0;
143 augment_leaf(&mut tree, value, to);
144 }
145 }
146
147 Compressed {
148 left_spill: tree[16],
149 right_spill: tree[31],
150 nodes: tree[2..16].try_into().unwrap(),
151 }
152}
153
154/// Augment the correct leaf by the given non-negative value. Walks up the tree starting at the
155/// given index until finding a leaf node. Storing the tree as an implicit structure has a nice
156/// benefit that finding the next left or right node is straightforward.
157fn augment_leaf(tree: &mut Snailfish, value: i32, mut to: usize) {
158 while tree[to] == -1 {
159 to /= 2;
160 }
161 tree[to] += value;
162}
163
164/// Split a node into two child nodes.
165///
166/// Search the tree starting with the leaves, splitting the first node over `10` found (if any).
167/// We can optimize the rules by immediately exploding if this happens in a node 4 levels deep.
168fn split(tree: &mut Snailfish) {
169 let mut i = 16;
170
171 while i < 32 {
172 if tree[i] == -1 {
173 let mut j = i / 2;
174 while tree[j] == -1 {
175 j /= 2;
176 }
177
178 if tree[j] >= 10 {
179 // Still room to add another layer of depth.
180 tree[2 * j] = tree[j] / 2;
181 tree[2 * j + 1] = (tree[j] + 1) / 2;
182 tree[j] = -1;
183 } else {
184 i += 1;
185 }
186 } else {
187 if tree[i] >= 10 {
188 // Avoid going too deep by performing the followup explode now.
189 if i > 16 {
190 let value = tree[i] / 2;
191 augment_leaf(tree, value, i - 1);
192 }
193 if i < 31 {
194 let value = (tree[i] + 1) / 2;
195 augment_leaf(tree, value, i + 1);
196 }
197 tree[i] = 0;
198 // Left node could now be over 10 and needs rechecking.
199 i = (i - 1).max(16);
200 } else {
201 i += 1;
202 }
203 }
204 }
205}
206
207/// Calculate the magnitude of a snailfish number in place without using recursion.
208///
209/// This operation is destructive but much faster than using a recursive approach and acceptable
210/// as we no longer need the original snailfish number afterward.
211fn magnitude(mut tree: Snailfish) -> i32 {
212 for i in (1..16).rev() {
213 if tree[i] == -1 {
214 tree[i] = 3 * tree[2 * i] + 2 * tree[2 * i + 1];
215 }
216 }
217 tree[1]
218}