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}