aoc/year2023/
day20.rs

1//! # Pulse Propagation
2//!
3//! Similar to [`Day 8`] the input has a very specific structure. The flip-flips form 4 rows
4//! of 12 columns, following by 2 conjunctions (in square brackets):
5//!
6//! ```none
7//!            / aa ab ac ad ae af ag ah ai aj aj al [ax] [ay] \
8//!           /  ba bb bc bd be bf bg bh bi bj bj bl [bx] [by]  \
9//!     () - ()                                                 [zz] -> [rx]
10//!           \  ca cb dc cd ce cf cg ch ci cj cj cl [cx] [cy]  /
11//!            \ da db dc dd de df dg dh di dj dj dl [dx] [dy] /
12//! ```
13//!
14//! The penultimate conjuction in each row, for example `ax` both takes input and delivers output
15//! to the flips flops. This follows a pattern, for example using `v` to indicate input from the
16//! conjunction and `v` to indicate output:
17//!
18//! ```none
19//!     v     v        v              v
20//!     aa ab ac ad ae af ag ah ai aj aj al
21//!     v  v     v  v     v  v  v   v     v
22//! ```
23//!
24//! The flip flops form a binary counter. When the counter reaches a specific value the conjunction
25//! will pulse low and reset the counter to zero. When all 4 counters hit their limit at the
26//! same time then a low pulse will be sent to `rx`. The answer is the
27//! [LCM](https://en.wikipedia.org/wiki/Least_common_multiple) of the 4 limit values.
28//!  For my input the numbers were co-prime so the LCM simplified to a product.
29//!
30//! For part one, as long as all numbers are greater than 1000, then the counting pulses follow
31//! a predictable pattern that we can calculate with some bitwise logic.
32//!
33//! [`Day 8`]: crate::year2023::day08
34use crate::util::hash::*;
35
36type Input = [u32; 4];
37
38pub fn parse(input: &str) -> Input {
39    // Build the graph
40    let mut node = FastMap::with_capacity(100);
41    let mut kind = FastMap::with_capacity(100);
42
43    for line in input.lines() {
44        let mut tokens = line.split(|c: char| !c.is_ascii_lowercase()).filter(|s| !s.is_empty());
45
46        let key = tokens.next().unwrap();
47        let children: Vec<_> = tokens.collect();
48
49        node.insert(key, children);
50        kind.insert(key, !line.starts_with('&'));
51    }
52
53    // Follow the nodes from the broadcaster node building each binary number.
54    let mut todo = Vec::new();
55    let mut numbers = Vec::new();
56
57    for &start in &node["broadcaster"] {
58        todo.push((start, 0, 1));
59    }
60
61    while let Some((key, mut value, bit)) = todo.pop() {
62        let children = &node[key];
63
64        if let Some(next) = children.iter().find(|&&k| kind[k]) {
65            if children.len() == 2 {
66                value |= bit;
67            }
68            todo.push((next, value, bit << 1));
69        } else {
70            numbers.push(value | bit);
71        }
72    }
73
74    numbers.try_into().unwrap()
75}
76
77/// Use bitwise logic to count pulses.
78pub fn part1(input: &Input) -> u32 {
79    // Counting only works correctly if there are no resets from 1 to 1000
80    // so that we can assume all rows increment exactly the same.
81    assert!(input.iter().all(|&n| n > 1000));
82
83    // Each conjunction feeds back into the chained flip-flops in the inverse pattern
84    // to the flip-flops feeding into the conjunction, except for the least significant
85    // flip-flop which is always set. Thus the total is 12 - count_ones + 1.
86    let pairs: Vec<_> = input.iter().map(|n| (n, 13 - n.count_ones())).collect();
87
88    // The button and broadcaster contribute 5 low pulses each press.
89    let mut low = 5000;
90    let mut high = 0;
91
92    for n in 0..1000 {
93        // Flip flop changing from off to on emits a high pulse.
94        let rising: u32 = !n & (n + 1);
95        high += 4 * rising.count_ones();
96
97        // Flip flop changing from on to off emits a low pulse.
98        let falling: u32 = n & !(n + 1);
99        low += 4 * falling.count_ones();
100
101        for &(number, feedback) in &pairs {
102            // Factor is the number of high pulses sent to the conjunction.
103            // For each pulse the conjunction feeds a high pulse back to "feedback" flip flops.
104            // In addition the penultimate conjunction in each row receives "factor" high pulses,
105            // resulting in "factor" low pulses the final conjunction and finally "factor" high
106            // pulses to "rx".
107            let factor = (rising & number).count_ones();
108            high += factor * (feedback + 3);
109            low += factor;
110
111            // Factor is the number of low pulses sent to the conjunction.
112            // For each pulse the conjunction feeds a high pulse back to "feedback" flip flops.
113            // In addition the penultimate conjunction in each row receives "factor" high pulses,
114            // resulting in "factor" low pulses the final conjunction and finally "factor" high
115            // pulses to "rx".
116            let factor = (falling & number).count_ones();
117            high += factor * (feedback + 2);
118            low += 2 * factor;
119        }
120    }
121
122    low * high
123}
124
125/// Assume all numbers are prime (or co-prime) so that the LCM is equal to the product.
126pub fn part2(input: &Input) -> u64 {
127    input.iter().map(|&n| n as u64).product()
128}