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
16//! the 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 assert!(
76 input.iter().all(|&n| n > 1000),
77 "Counting only works correctly if there are no resets from 1 to 1000 \
78 so that we can assume all rows increment exactly the same."
79 );
80
81 // Each conjunction feeds back into the chained flip-flops in the inverse pattern
82 // to the flip-flops feeding into the conjunction, except for the least significant
83 // flip-flop which is always set. Thus the total is 12 - count_ones + 1.
84 let pairs = input.map(|n| (n, 13 - n.count_ones()));
85
86 // The button and broadcaster contribute 5 low pulses each press.
87 let mut low = 5000;
88 let mut high = 0;
89
90 for n in 0..1000 {
91 // Flip flop changing from off to on emits a high pulse.
92 let rising: u32 = !n & (n + 1);
93 high += 4 * rising.count_ones();
94
95 // Flip flop changing from on to off emits a low pulse.
96 let falling = n & !(n + 1);
97 low += 4 * falling.count_ones();
98
99 for (number, feedback) in pairs {
100 // Factor is the number of high pulses sent to the conjunction.
101 // For each pulse the conjunction feeds a high pulse back to "feedback" flip-flops.
102 // In addition, the penultimate conjunction in each row receives "factor" high pulses,
103 // resulting in "factor" low pulses to the final conjunction and finally "factor" high
104 // pulses to "rx".
105 let factor = (rising & number).count_ones();
106 high += factor * (feedback + 3);
107 low += factor;
108
109 // Factor is the number of low pulses sent to the conjunction.
110 // For each pulse the conjunction feeds a high pulse back to "feedback" flip-flops.
111 // In addition, the penultimate conjunction in each row receives "factor" high pulses,
112 // resulting in "factor" low pulses to the final conjunction and finally "factor" high
113 // pulses to "rx".
114 let factor = (falling & number).count_ones();
115 high += factor * (feedback + 2);
116 low += 2 * factor;
117 }
118 }
119
120 low * high
121}
122
123/// Assume all numbers are prime (or co-prime) so that the LCM is equal to the product.
124pub fn part2(input: &Input) -> u64 {
125 input.iter().map(|&n| n as u64).product()
126}