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}