Skip to content

Commit

Permalink
Day 20 solutions
Browse files Browse the repository at this point in the history
  • Loading branch information
Cadiac committed Dec 20, 2023
1 parent 3d58e71 commit d2c9c0b
Show file tree
Hide file tree
Showing 5 changed files with 348 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,4 @@ This should start the server at `localhost:8080`.
❄️ [Day 17](aoc-solver/src/y2023/day17.rs)
❄️ [Day 18](aoc-solver/src/y2023/day18.rs)
❄️ [Day 19](aoc-solver/src/y2023/day19.rs)
❄️ [Day 20](aoc-solver/src/y2023/day20.rs)
284 changes: 284 additions & 0 deletions aoc-solver/src/y2023/day20.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
use std::collections::{HashMap, VecDeque};

use itertools::Itertools;

use crate::solution::{AocError, Solution};

pub struct Day20;

#[derive(PartialEq, Eq)]
enum ModuleType {
FlipFlop,
Conjunction,
Neutral,
}

struct Module {
kind: ModuleType,
state: bool,
memory: HashMap<String, bool>,
inputs: Vec<String>,
outputs: Vec<String>,
}

fn parse(input: &str) -> Result<HashMap<String, Module>, AocError> {
let mut modules: HashMap<String, Module> = input
.lines()
.map(|line| {
let (name, outputs) = line
.split_once(" -> ")
.ok_or(AocError::parse(line, "Missing ' -> ' separator"))?;

let outputs = outputs
.split(", ")
.map(|output| output.to_owned())
.collect();

let (kind, name) = match name.chars().next() {
Some('%') => (
ModuleType::FlipFlop,
name.strip_prefix('%').ok_or(AocError::parse(name, "%"))?,
),
Some('&') => (
ModuleType::Conjunction,
name.strip_prefix('&').ok_or(AocError::parse(name, "%"))?,
),
_ => (ModuleType::Neutral, name),
};

Ok((
name.to_owned(),
Module {
kind,
state: false, // FlipFlops
memory: HashMap::new(), // Conjunctions
inputs: Vec::new(),
outputs,
},
))
})
.try_collect()?;

let mut inputs: HashMap<String, Vec<String>> = HashMap::new();

// Construct module inputs
for (name, module) in modules.iter() {
for output in &module.outputs {
inputs.entry(output.clone()).or_default().push(name.clone());
}
}

// Fill in the inputs and conjunction memories
for (name, module) in modules.iter_mut() {
if let Some(module_inputs) = inputs.get(name) {
for input in module_inputs {
module.inputs.push(input.clone());
if module.kind == ModuleType::Conjunction {
module.memory.insert(input.clone(), false);
}
}
}
}

Ok(modules)
}

fn press_button(
button_press: u32,
modules: &mut HashMap<String, Module>,
modules_to_find: &mut HashMap<String, Option<u32>>,
) -> (u32, u32) {
let mut output_buffer =
VecDeque::from([(String::from("broadcaster"), false, String::from("button"))]);

let mut lows = 1;
let mut highs = 0;

while let Some((current, input, source)) = output_buffer.pop_front() {
if let Some(module) = modules.get_mut(&current) {
let output = match module.kind {
ModuleType::Neutral => Some(input),
ModuleType::Conjunction => {
module.memory.insert(source, input);
let output = module.memory.values().any(|value| !value);
if output {
for (to_find, presses) in modules_to_find.iter_mut() {
if current == *to_find {
*presses = Some(button_press);
}
}
}
Some(output)
}
ModuleType::FlipFlop => {
if !input {
module.state = !module.state;
Some(module.state)
} else {
None
}
}
};

if let Some(output) = output {
emit_output(
output,
&current,
module,
&mut output_buffer,
&mut lows,
&mut highs,
);
}
}
}

(lows, highs)
}

fn emit_output(
output: bool,
source: &str,
module: &mut Module,
output_buffer: &mut VecDeque<(String, bool, String)>,
lows: &mut u32,
highs: &mut u32,
) {
match output {
true => *highs += module.outputs.len() as u32,
false => *lows += module.outputs.len() as u32,
}

for target in &module.outputs {
output_buffer.push_back((target.clone(), output, source.to_owned()));
}
}

fn gcd(a: u64, b: u64) -> u64 {
if b == 0 {
a
} else {
gcd(b, a % b)
}
}

fn lcm(a: u64, b: u64) -> u64 {
a / gcd(a, b) * b
}

impl Solution for Day20 {
type A = u64;
type B = u64;

fn default_input(&self) -> &'static str {
include_str!("../../../inputs/2023/day20.txt")
}

fn part_1(&self, input: &str) -> Result<u64, AocError> {
let mut modules = parse(input)?;

let mut lows = 0;
let mut highs = 0;

for button_press in 0..1000 {
let pulses = press_button(button_press, &mut modules, &mut HashMap::new());
lows += pulses.0;
highs += pulses.1;
}

Ok(lows as u64 * highs as u64)
}

fn part_2(&self, input: &str) -> Result<u64, AocError> {
let mut modules = parse(input)?;

let (rx, _) = modules
.iter()
.find(|(_, module)| module.outputs.contains(&String::from("rx")))
.ok_or(AocError::logic("Missing rx module"))?;

let mut current = rx.as_str();
let mut modules_to_find: HashMap<String, Option<u32>> = HashMap::new();

// This isn't that generic solution - assuming that chain of conjunction modules
// produces leads to the 'rx' module traverse this chain until we see conjunction
// module with more than one input. These inputs will trigger periodically, find
// the answer by determining the LCM of those periodic input cycle durations.
loop {
let target_module = modules
.get(current)
.ok_or(AocError::logic("Missing module"))?;

if target_module.kind != ModuleType::Conjunction {
return Err(AocError::logic("Chain of conjunction must lead to rx"));
}

// In this puzzle picking the first module with more than one input already
// had a period of ~4000 button presses, but if these cycles were longer or
// more expensive to calculate we could continue deeper and just do more LCMs
if target_module.inputs.len() > 1 {
for input in &target_module.inputs {
modules_to_find.insert(input.clone(), None);
}
break;
}

current = target_module.inputs[0].as_str();
}

let mut button_press = 1;

loop {
press_button(button_press, &mut modules, &mut modules_to_find);

if modules_to_find.values().all(|value| value.is_some()) {
let fewest_presses = modules_to_find
.values()
.map(|value| value.unwrap_or(0))
.fold(1, |acc, period| lcm(acc, std::cmp::max(period as u64, 1)));

return Ok(fewest_presses);
}

button_press += 1;
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_solves_part1_example_1() {
assert_eq!(
Day20.part_1(
"broadcaster -> a, b, c\n\
%a -> b\n\
%b -> c\n\
%c -> inv\n\
&inv -> a\n"
),
Ok(32000000)
);
}

#[test]
fn it_solves_part1_example_2() {
assert_eq!(
Day20.part_1(
"broadcaster -> a\n\
%a -> inv, con\n\
&inv -> b\n\
%b -> con\n\
&con -> output\n"
),
Ok(11687500)
);
}

#[test]
fn it_solves_part2_real() {
assert_eq!(Day20.part_2(Day20.default_input()), Ok(238920142622879));
}
}
5 changes: 4 additions & 1 deletion aoc-solver/src/y2023/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ pub mod day16;
pub mod day17;
pub mod day18;
pub mod day19;
pub mod day20;

pub const MAX_DAYS: u8 = 19;
pub const MAX_DAYS: u8 = 20;

pub struct Y2023;

Expand All @@ -46,6 +47,7 @@ impl Solver for Y2023 {
17 => day17::Day17.run(input, 17, 2023),
18 => day18::Day18.run(input, 18, 2023),
19 => day19::Day19.run(input, 19, 2023),
20 => day20::Day20.run(input, 20, 2023),
_ => vec![String::from("Solution not implemented (yet?)")],
}
}
Expand Down Expand Up @@ -82,6 +84,7 @@ impl Solver for Y2023 {
17 => include_str!("./day17.rs"),
18 => include_str!("./day18.rs"),
19 => include_str!("./day19.rs"),
20 => include_str!("./day20.rs"),
_ => unimplemented!(),
}
}
Expand Down
1 change: 1 addition & 0 deletions aoc-web/src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub fn header(props: &HeaderProps) -> Html {
<NavLink route={Route::Solution { year: 2023, day: 17 }} current={props.route.clone()} text={"17"}/>
<NavLink route={Route::Solution { year: 2023, day: 18 }} current={props.route.clone()} text={"18"}/>
<NavLink route={Route::Solution { year: 2023, day: 19 }} current={props.route.clone()} text={"19"}/>
<NavLink route={Route::Solution { year: 2023, day: 20 }} current={props.route.clone()} text={"20"}/>
</>
}
},
Expand Down
58 changes: 58 additions & 0 deletions inputs/2023/day20.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
&ls -> fg, ck, fv, sk, fl, hz
%px -> ls, dv
%jk -> xt, tx
%hd -> fs, vn
%pk -> ql
%bj -> tx, pr
%vg -> xl, sf
%cj -> bj
%sk -> bz
%fl -> fx
%th -> fl, ls
%pr -> gm
%xv -> sf, hp
%mh -> jb
%jh -> kx, tx
%jz -> pm, fs
%hr -> tx, gk
%kx -> cj
%ql -> jx
%gm -> tx, jt
%hz -> ls, fv
%dt -> fs
%gg -> sf, vg
%xl -> fh
&pq -> vr
%jx -> mv
%kr -> gg
%bn -> px, ls
&fs -> pm, rg, pq, pj, nk, mh, jb
%vn -> rt, fs
%jt -> tx, zb
broadcaster -> hz, hr, nk, nv
%fx -> sk, ls
%rt -> fs, kz
%gk -> tx, jh
%mv -> sf, kr
%bz -> bn, ls
%dv -> ls
%rg -> jz
%pj -> mh
%kz -> dt, fs
%ck -> hg
&fg -> vr
&sf -> pk, jx, nv, kr, xl, ql, dk
%jb -> rg
%nk -> fs, pj
&dk -> vr
%hp -> sf
&tx -> hr, fm, kx, cj, pr
%nv -> pk, sf
%fh -> xv, sf
%xt -> tx
%hg -> ls, th
%zb -> tx, jk
&fm -> vr
%pm -> hd
%fv -> ck
&vr -> rx

0 comments on commit d2c9c0b

Please sign in to comment.