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 mut sum = add(&input[0], &input[1]);
81
82 for next in &input[2..] {
83 sum = add(&compress(sum), next);
84 }
85
86 magnitude(sum)
87}
88
89/// Find the largest magnitude of any two snailfish numbers, remembering that snailfish addition
90/// is *not* commutative.
91pub fn part2(input: &[Compressed]) -> i32 {
92 // Use as many cores as possible to parallelize the calculation.
93 let result = spawn_parallel_iterator(input, |iter| {
94 iter.flat_map(|a| {
95 // Avoid pairing `a` with itself.
96 let index = input.element_offset(a).unwrap();
97 input[..index].iter().chain(&input[index + 1..]).map(|b| magnitude(add(a, b)))
98 })
99 .max()
100 });
101
102 result.into_iter().flatten().max().unwrap()
103}
104
105/// Add two snailfish numbers.
106///
107/// The initial step creates a new root node then makes the numbers the left and right children
108/// of this new root node, by copying the respective ranges of the implicit trees.
109///
110/// We can optimize the rules a little; the first round of explode was already done in creating
111/// compressed arguments, and a split runs its own inline explode.
112fn add(left: &Compressed, right: &Compressed) -> Snailfish {
113 let mut tree = [-1; 32];
114
115 // Copy left into place.
116 tree[4..6].copy_from_slice(&left.nodes[0..2]);
117 tree[8..12].copy_from_slice(&left.nodes[2..6]);
118 tree[16..24].copy_from_slice(&left.nodes[6..14]);
119
120 // Copy right into place.
121 tree[6..8].copy_from_slice(&right.nodes[0..2]);
122 tree[12..16].copy_from_slice(&right.nodes[2..6]);
123 tree[24..32].copy_from_slice(&right.nodes[6..14]);
124
125 // Adjust by the explode spillover between sides.
126 match (right.left_spill, left.right_spill) {
127 (-1, -1) => (),
128 (left_spill, -1) => augment_leaf(&mut tree, left_spill, 23),
129 (-1, right_spill) => augment_leaf(&mut tree, right_spill, 24),
130 (left_spill, right_spill) => tree[23] += left_spill + right_spill,
131 }
132
133 // Now we process all split operations; any further explode actions are done during any split
134 // that creates a temporary depth 5.
135 split(&mut tree);
136 tree
137}
138
139/// Perform all initial explodes to create a compressed number from a snailfish number.
140/// This is a destructive operation, as no caller needs the original afterwards.
141fn compress(mut tree: Snailfish) -> Compressed {
142 for from in 17..31 {
143 let to = if from % 2 == 0 { from / 2 - 1 } else { from + 1 };
144 let value = tree[from];
145 if value >= 0 {
146 tree[from / 2] = 0;
147 augment_leaf(&mut tree, value, to);
148 }
149 }
150
151 Compressed {
152 left_spill: tree[16],
153 right_spill: tree[31],
154 nodes: tree[2..16].try_into().unwrap(),
155 }
156}
157
158/// Augment the correct leaf by the given non-negative value. Walks up the tree starting at the
159/// given index until finding a leaf node. Storing the tree as an implicit structure has a nice
160/// benefit that finding the next left or right node is straightforward.
161fn augment_leaf(tree: &mut Snailfish, value: i32, mut to: usize) {
162 while tree[to] == -1 {
163 to /= 2;
164 }
165 tree[to] += value;
166}
167
168/// Split a node into two child nodes.
169///
170/// Search the tree starting with the leaves, splitting the first node over `10` found (if any).
171/// We can optimize the rules by immediately exploding if this happens in a node 4 levels deep.
172fn split(tree: &mut Snailfish) {
173 let mut i = 16;
174
175 while i < 32 {
176 if tree[i] == -1 {
177 let mut j = i / 2;
178 while tree[j] == -1 {
179 j /= 2;
180 }
181
182 if tree[j] >= 10 {
183 // Still room to add another layer of depth.
184 tree[2 * j] = tree[j] / 2;
185 tree[2 * j + 1] = (tree[j] + 1) / 2;
186 tree[j] = -1;
187 } else {
188 i += 1;
189 }
190 } else {
191 if tree[i] >= 10 {
192 // Avoid going too deep by performing the followup explode now.
193 if i > 16 {
194 let value = tree[i] / 2;
195 augment_leaf(tree, value, i - 1);
196 }
197 if i < 31 {
198 let value = (tree[i] + 1) / 2;
199 augment_leaf(tree, value, i + 1);
200 }
201 tree[i] = 0;
202 // Left node could now be over 10 and needs rechecking.
203 i = (i - 1).max(16);
204 } else {
205 i += 1;
206 }
207 }
208 }
209}
210
211/// Calculate the magnitude of a snailfish number in place without using recursion.
212///
213/// This operation is destructive but much faster than using a recursive approach and acceptable
214/// as we no longer need the original snailfish number afterward.
215fn magnitude(mut tree: Snailfish) -> i32 {
216 for i in (1..16).rev() {
217 if tree[i] == -1 {
218 tree[i] = 3 * tree[2 * i] + 2 * tree[2 * i + 1];
219 }
220 }
221 tree[1]
222}