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 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.
30use crate::util::thread::*;
31use std::sync::atomic::{AtomicI32, Ordering};
32
33type Snailfish = [i32; 63];
34
35/// The indices for [in-order traversal](https://en.wikipedia.org/wiki/Tree_traversal) of the first
36/// 4 levels of the implicit binary tree stored in an array.
37const IN_ORDER: [usize; 30] = [
38 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,
39 28, 14, 29, 30,
40];
41
42/// Parse a snailfish number into an implicit binary tree stored in an array.
43///
44/// Since no number will greater than 9 initially we can consider each character individually.
45/// `[` means moves down a level to parse children, `,` means move from left to right node,
46/// `]` means move up a level to return to parent and a digit from 0-9 creates a leaf node
47/// with that value.
48pub fn parse(input: &str) -> Vec<Snailfish> {
49 fn helper(bytes: &[u8]) -> Snailfish {
50 let mut tree = [-1; 63];
51 let mut i = 0;
52
53 for &b in bytes {
54 match b {
55 b'[' => i = 2 * i + 1,
56 b',' => i += 1,
57 b']' => i = (i - 1) / 2,
58 b => tree[i] = (b - 48) as i32,
59 }
60 }
61
62 tree
63 }
64 input.lines().map(|line| helper(line.as_bytes())).collect()
65}
66
67/// Add all snailfish numbers, reducing to a single magnitude.
68pub fn part1(input: &[Snailfish]) -> i32 {
69 let mut sum = input.iter().copied().reduce(|acc, n| add(&acc, &n)).unwrap();
70 magnitude(&mut sum)
71}
72
73/// Find the largest magnitude of any two snailfish numbers, remembering that snailfish addition
74/// is *not* commutative.
75pub fn part2(input: &[Snailfish]) -> i32 {
76 let mut pairs = Vec::new();
77
78 for (i, a) in input.iter().enumerate() {
79 for (j, b) in input.iter().enumerate() {
80 if i != j {
81 pairs.push((a, b));
82 }
83 }
84 }
85
86 // Use as many cores as possible to parallelize the calculation.
87 let shared = AtomicI32::new(0);
88 spawn_parallel_iterator(&pairs, |iter| worker(&shared, iter));
89 shared.load(Ordering::Relaxed)
90}
91
92/// Pair addition is independent so we can parallelize across multiple threads.
93fn worker(shared: &AtomicI32, iter: ParIter<'_, (&Snailfish, &Snailfish)>) {
94 let mut partial = 0;
95
96 for (a, b) in iter {
97 partial = partial.max(magnitude(&mut add(a, b)));
98 }
99
100 shared.fetch_max(partial, Ordering::Relaxed);
101}
102
103/// Add two snailfish numbers.
104///
105/// The initial step creates a new root node then makes the numbers the left and right children
106/// of this new root node, by copying the respective ranges of the implicit trees.
107///
108/// We can optimize the rules a little. This initial combination is the only time that more than one
109/// pair will be 4 levels deep simultaneously, so we can sweep from left to right on all possible
110/// leaf nodes in one pass.
111fn add(left: &Snailfish, right: &Snailfish) -> Snailfish {
112 let mut tree = [-1; 63];
113
114 tree[3..5].copy_from_slice(&left[1..3]);
115 tree[7..11].copy_from_slice(&left[3..7]);
116 tree[15..23].copy_from_slice(&left[7..15]);
117 tree[31..47].copy_from_slice(&left[15..31]);
118
119 tree[5..7].copy_from_slice(&right[1..3]);
120 tree[11..15].copy_from_slice(&right[3..7]);
121 tree[23..31].copy_from_slice(&right[7..15]);
122 tree[47..63].copy_from_slice(&right[15..31]);
123
124 for pair in (31..63).step_by(2) {
125 if tree[pair] >= 0 {
126 explode(&mut tree, pair);
127 }
128 }
129
130 while split(&mut tree) {}
131 tree
132}
133
134/// Explode a specific pair identified by an index.
135///
136/// Storing the tree as an implicit structure has a nice benefit that finding the next left or right
137/// node is straightforward. We first move to the next left or right leaf node by adding or
138/// subtracting one to the index. If this node is empty then we move to the parent node until we
139/// find a leaf node.
140///
141/// The leaf node at index 31 has no possible nodes to the left and similarly the leaf node at
142/// index 62 has no possible nodes to the right.
143fn explode(tree: &mut Snailfish, pair: usize) {
144 if pair > 31 {
145 let mut i = pair - 1;
146 loop {
147 if tree[i] >= 0 {
148 tree[i] += tree[pair];
149 break;
150 }
151 i = (i - 1) / 2;
152 }
153 }
154
155 if pair < 61 {
156 let mut i = pair + 2;
157 loop {
158 if tree[i] >= 0 {
159 tree[i] += tree[pair + 1];
160 break;
161 }
162 i = (i - 1) / 2;
163 }
164 }
165
166 tree[pair] = -1;
167 tree[pair + 1] = -1;
168 tree[(pair - 1) / 2] = 0;
169}
170
171/// Split a node into two child nodes.
172///
173/// Search the tree in an *in-order* traversal, splitting the first node over `10` found (if any).
174/// We can optimize the rules by immediately exploding if this results in a node 4 levels deep,
175/// as we know that the prior optimzation in the [`add`] function means that this is the only
176/// explosion possible.
177fn split(tree: &mut Snailfish) -> bool {
178 for &i in &IN_ORDER {
179 if tree[i] >= 10 {
180 tree[2 * i + 1] = tree[i] / 2;
181 tree[2 * i + 2] = (tree[i] + 1) / 2;
182 tree[i] = -1;
183
184 if i >= 15 {
185 explode(tree, 2 * i + 1);
186 }
187 return true;
188 }
189 }
190 false
191}
192
193/// Calculate the magnitude of a snailfish number in place without using recursion.
194///
195/// This operation is destructive but much faster than using a recursive approach and acceptable
196/// as we no longer need the original snailfish number afterwards.
197fn magnitude(tree: &mut Snailfish) -> i32 {
198 for i in (0..31).rev() {
199 if tree[i] == -1 {
200 tree[i] = 3 * tree[2 * i + 1] + 2 * tree[2 * i + 2];
201 }
202 }
203 tree[0]
204}