Skip to main content

aoc/year2017/
day16.rs

1//! # Permutation Promenade
2//!
3//! The key insight is that a complete dance can be represented by just two transformations.
4//! The spin and exchange moves compose into a single transformation and the partner swaps compose
5//! into a second independent transformation.
6//!
7//! Each transformation can then be applied to itself to double the effect. For example, a single
8//! complete dance turns into two dances, then doubles to four dances and so on.
9//!
10//! This allows us to compute part two with a similar approach to
11//! [exponentiation by squaring](https://en.wikipedia.org/wiki/Exponentiation_by_squaring).
12use std::array::from_fn;
13
14use crate::util::parse::*;
15
16#[derive(Clone, Copy)]
17pub struct Dance {
18    /// The letter in each position from left to right
19    /// with `a` represented by 0, `b` by 1 and so on.
20    position: [usize; 16],
21    /// A map of initial letter to final letter taking into account all partner swaps.
22    /// `a` is at index 0, `b` at index 1. For convenience letters are represented by 0..15.
23    exchange: [usize; 16],
24}
25
26impl Dance {
27    /// Converts a Dance into a string representation.
28    fn apply(self) -> String {
29        self.position.iter().map(|&i| to_char(self.exchange[i])).collect()
30    }
31
32    /// Combines two Dances into a new Dance.
33    fn compose(self, other: Self) -> Self {
34        let position = self.position.map(|i| other.position[i]);
35        let exchange = self.exchange.map(|i| other.exchange[i]);
36        Self { position, exchange }
37    }
38}
39
40/// Reduces all 10,000 individual dance moves into just two independent transformations.
41pub fn parse(input: &str) -> Dance {
42    // Tokenize the input into two parallel iterators.
43    let mut letters = input.bytes().filter(u8::is_ascii_lowercase);
44    let mut numbers = input.iter_unsigned::<usize>();
45
46    // Start from the identity transformation.
47    let mut offset = 0;
48    let mut lookup: [usize; 16] = from_fn(|i| i);
49    let mut position: [usize; 16] = from_fn(|i| i);
50    let mut exchange: [usize; 16] = from_fn(|i| i);
51
52    while let Some(op) = letters.next() {
53        match op {
54            // Increasing the offset has the same effect as rotating elements to the right.
55            b's' => offset += 16 - numbers.next().unwrap(),
56            // Swap two elements taking into account the offset when calculating indices.
57            b'x' => {
58                let first = numbers.next().unwrap();
59                let second = numbers.next().unwrap();
60                position.swap((first + offset) % 16, (second + offset) % 16);
61            }
62            // First lookup the index of each letter, then swap the mapping.
63            b'p' => {
64                let first = from_byte(letters.next().unwrap());
65                let second = from_byte(letters.next().unwrap());
66                lookup.swap(first, second);
67                exchange.swap(lookup[first], lookup[second]);
68            }
69            _ => unreachable!(),
70        }
71    }
72
73    // Rotate the array once to apply all spins.
74    position.rotate_left(offset % 16);
75
76    Dance { position, exchange }
77}
78
79/// Apply the transformation once.
80pub fn part1(input: &Dance) -> String {
81    input.apply()
82}
83
84/// Repeatedly applying a transformation to itself allows the computation of exponentially
85/// more dances, until reaching the complete 1 billion transformations.
86pub fn part2(input: &Dance) -> String {
87    let mut dance = *input;
88
89    // 1 billion is 0b00111011_10011010_11001010_00000000, which is 30 bits, with 13 set. Typical
90    // exponentiation by squaring would be 30 doubles and 13 additions, or 43 calls to
91    // compose. Since one billion is a power of ten, we can do better by 9 cycles of
92    // reaching each next power of ten by two doubles, one addition, and one more double
93    // per cycle, for a total of only 36 calls to compose.
94    for _ in 0..9 {
95        let dance2 = dance.compose(dance);
96        let dance5 = dance2.compose(dance2).compose(dance);
97        dance = dance5.compose(dance5);
98    }
99
100    dance.apply()
101}
102
103fn from_byte(b: u8) -> usize {
104    (b - b'a') as usize
105}
106
107fn to_char(i: usize) -> char {
108    ((i as u8) + b'a') as char
109}