Skip to main content

aoc/year2023/
day20.rs

1//! # Pulse Propagation
2//!
3//! Similar to [`Day 8`] the input has a very specific structure. The flip-flops form 4 rows
4//! of 12 columns, followed by 2 conjunctions (in square brackets):
5//!
6//! ```none
7//!            / aa ab ac ad ae af ag ah ai aj ak al [ax] [ay] \
8//!           /  ba bb bc bd be bf bg bh bi bj bk bl [bx] [by]  \
9//!     () - ()                                                 [zz] -> [rx]
10//!           \  ca cb cc cd ce cf cg ch ci cj ck cl [cx] [cy]  /
11//!            \ da db dc dd de df dg dh di dj dk dl [dx] [dy] /
12//! ```
13//!
14//! The penultimate conjunction in each row, for example `ax` both takes input and delivers output
15//! to the flip-flops. This follows a pattern, for example, using `v` above to indicate input from the
16//! conjunction and `v` below to indicate output:
17//!
18//! ```none
19//!     v     v        v              v
20//!     aa ab ac ad ae af ag ah ai aj ak 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<_> = node["broadcaster"].iter().map(|&start| (start, 0, 1)).collect();
55    let mut numbers = Vec::new();
56
57    while let Some((key, mut value, bit)) = todo.pop() {
58        let children = &node[key];
59
60        if let Some(next) = children.iter().find(|&&k| kind[k]) {
61            if children.len() == 2 {
62                value |= bit;
63            }
64            todo.push((next, value, bit << 1));
65        } else {
66            numbers.push(value | bit);
67        }
68    }
69
70    numbers.try_into().unwrap()
71}
72
73/// Use bitwise logic to count pulses.
74pub fn part1(input: &Input) -> u32 {
75    // Counting only works correctly if there are no resets from 1 to 1000
76    // so that we can assume all rows increment exactly the same.
77    assert!(input.iter().all(|&n| n > 1000));
78
79    // Each conjunction feeds back into the chained flip-flops in the inverse pattern
80    // to the flip-flops feeding into the conjunction, except for the least significant
81    // flip-flop which is always set. Thus the total is 12 - count_ones + 1.
82    let pairs: Vec<_> = input.iter().map(|n| (n, 13 - n.count_ones())).collect();
83
84    // The button and broadcaster contribute 5 low pulses each press.
85    let mut low = 5000;
86    let mut high = 0;
87
88    for n in 0..1000 {
89        // Flip flop changing from off to on emits a high pulse.
90        let rising: u32 = !n & (n + 1);
91        high += 4 * rising.count_ones();
92
93        // Flip flop changing from on to off emits a low pulse.
94        let falling: u32 = n & !(n + 1);
95        low += 4 * falling.count_ones();
96
97        for &(number, feedback) in &pairs {
98            // Factor is the number of high pulses sent to the conjunction.
99            // For each pulse the conjunction feeds a high pulse back to "feedback" flip-flops.
100            // In addition, the penultimate conjunction in each row receives "factor" high pulses,
101            // resulting in "factor" low pulses to the final conjunction and finally "factor" high
102            // pulses to "rx".
103            let factor = (rising & number).count_ones();
104            high += factor * (feedback + 3);
105            low += factor;
106
107            // Factor is the number of low pulses sent to the conjunction.
108            // For each pulse the conjunction feeds a high pulse back to "feedback" flip-flops.
109            // In addition, the penultimate conjunction in each row receives "factor" high pulses,
110            // resulting in "factor" low pulses to the final conjunction and finally "factor" high
111            // pulses to "rx".
112            let factor = (falling & number).count_ones();
113            high += factor * (feedback + 2);
114            low += 2 * factor;
115        }
116    }
117
118    low * high
119}
120
121/// Assume all numbers are prime (or co-prime) so that the LCM is equal to the product.
122pub fn part2(input: &Input) -> u64 {
123    input.iter().map(|&n| n as u64).product()
124}