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}