From ee2bc2bc73b637aee0abde11d18dd4d5c36b7ebe Mon Sep 17 00:00:00 2001 From: Anthony Oteri Date: Wed, 20 Dec 2023 17:08:12 -0500 Subject: [PATCH] Day 20 - Part 1 Signed-off-by: Anthony Oteri --- day-20/Cargo.toml | 29 +++++ day-20/benches/benchmark.rs | 15 +++ day-20/input.txt | 58 +++++++++ day-20/src/bin/part1.rs | 21 ++++ day-20/src/bin/part2.rs | 21 ++++ day-20/src/error.rs | 9 ++ day-20/src/lib.rs | 4 + day-20/src/part1.rs | 230 ++++++++++++++++++++++++++++++++++++ day-20/src/part2.rs | 18 +++ day-20/test-input.txt | 5 + day-20/test-input2.txt | 5 + 11 files changed, 415 insertions(+) create mode 100644 day-20/Cargo.toml create mode 100644 day-20/benches/benchmark.rs create mode 100644 day-20/input.txt create mode 100644 day-20/src/bin/part1.rs create mode 100644 day-20/src/bin/part2.rs create mode 100644 day-20/src/error.rs create mode 100644 day-20/src/lib.rs create mode 100644 day-20/src/part1.rs create mode 100644 day-20/src/part2.rs create mode 100644 day-20/test-input.txt create mode 100644 day-20/test-input2.txt diff --git a/day-20/Cargo.toml b/day-20/Cargo.toml new file mode 100644 index 0000000..a4a4bd2 --- /dev/null +++ b/day-20/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "day-20" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +itertools = { workspace = true } +nom = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +miette = { workspace = true } +thiserror = { workspace = true } +dhat = { workspace = true } + +[dev-dependencies] +divan = { workspace = true } +env_logger = { workspace = true } +test-log = { workspace = true } +rstest = { workspace = true } + +[[bench]] +name = "day-20" +path = "benches/benchmark.rs" +harness = false + +[features] +dhat-heap = [] diff --git a/day-20/benches/benchmark.rs b/day-20/benches/benchmark.rs new file mode 100644 index 0000000..141dc70 --- /dev/null +++ b/day-20/benches/benchmark.rs @@ -0,0 +1,15 @@ +use day_20::*; + +fn main() { + divan::main(); +} + +#[divan::bench] +fn part1() { + part1::process(divan::black_box(include_str!("../input.txt"))).unwrap(); +} + +#[divan::bench] +fn part2() { + part2::process(divan::black_box(include_str!("../input.txt"))).unwrap(); +} diff --git a/day-20/input.txt b/day-20/input.txt new file mode 100644 index 0000000..be88ffa --- /dev/null +++ b/day-20/input.txt @@ -0,0 +1,58 @@ +%ls -> cs, jd +%st -> jh, bj +%hc -> tn +%xs -> rr +%pq -> tl, cs +%rj -> km, ck +%lb -> cg +%zz -> dx, ch +%kk -> cs, tc +%hr -> ck, pl +%pj -> cs, pr +%pr -> zl, cs +%tl -> vn, cs +%tc -> cs, hn +&cs -> hn, pj, qb, zl +%lv -> hr +%vn -> cs, ls +&mp -> dr +broadcaster -> ns, pj, xz, sg +%gg -> jh, nx +%cr -> jg, jh +%ch -> dx +&dx -> zj, xz, mp, zn, xs, hc +%fl -> jh, ps +%hn -> pq +%qp -> dx, zj +%bp -> zr +%kl -> lb +%pl -> ck, xq +%rm -> zz, dx +%rz -> jh, fl +%pm -> cs, kk +%zl -> pm +%rk -> ck, kl +&dr -> rx +%cg -> lv, ck +&qt -> dr +%jg -> jh +&qb -> dr +&ck -> lb, lv, ns, kl, qt +%zr -> st +%dd -> dx, qp +%kd -> rk, ck +%xq -> rj, ck +%sg -> rz, jh +%zj -> hc +%tn -> dx, xs +%jd -> cs +%rr -> rm, dx +&jh -> ng, bp, zr, sg, bj +%km -> ck +%ps -> bp, jh +%zn -> dd +%bj -> gg +%nx -> jh, cr +%xz -> dx, zn +&ng -> dr +%ns -> ck, kd \ No newline at end of file diff --git a/day-20/src/bin/part1.rs b/day-20/src/bin/part1.rs new file mode 100644 index 0000000..a416f23 --- /dev/null +++ b/day-20/src/bin/part1.rs @@ -0,0 +1,21 @@ +use day_20::part1::process; +use miette::Context; + +#[cfg(feature = "dhat-heap")] +#[global_allocator] +static ALLOC: dhat::Alloc = dhat::Alloc; + +#[tracing::instrument] +fn main() -> miette::Result<()> { + #[cfg(feature = "dhat-heap")] + let _profiler = dhat::Profiler::new_heap(); + + #[cfg(not(feature = "dhat-heap"))] + tracing_subscriber::fmt::init(); + + let file = include_str!("../../input.txt"); + let result = process(file).context("process part 1")?; + + println!("{}", result); + Ok(()) +} diff --git a/day-20/src/bin/part2.rs b/day-20/src/bin/part2.rs new file mode 100644 index 0000000..f7b024d --- /dev/null +++ b/day-20/src/bin/part2.rs @@ -0,0 +1,21 @@ +use day_20::part2::process; +use miette::Context; + +#[cfg(feature = "dhat-heap")] +#[global_allocator] +static ALLOC: dhat::Alloc = dhat::Alloc; + +#[tracing::instrument] +fn main() -> miette::Result<()> { + #[cfg(feature = "dhat-heap")] + let _profiler = dhat::Profiler::new_heap(); + + #[cfg(not(feature = "dhat-heap"))] + tracing_subscriber::fmt::init(); + + let file = include_str!("../../input.txt"); + let result = process(file).context("process part 1")?; + + println!("{}", result); + Ok(()) +} diff --git a/day-20/src/error.rs b/day-20/src/error.rs new file mode 100644 index 0000000..e08a17f --- /dev/null +++ b/day-20/src/error.rs @@ -0,0 +1,9 @@ +use miette::Diagnostic; +use thiserror::Error; + +#[derive(Error, Diagnostic, Debug)] +pub enum AocError { + #[error(transparent)] + #[diagnostic(code(aoc::io_error))] + IoError(#[from] std::io::Error), +} diff --git a/day-20/src/lib.rs b/day-20/src/lib.rs new file mode 100644 index 0000000..681e54b --- /dev/null +++ b/day-20/src/lib.rs @@ -0,0 +1,4 @@ +pub mod error; + +pub mod part1; +pub mod part2; diff --git a/day-20/src/part1.rs b/day-20/src/part1.rs new file mode 100644 index 0000000..5b5f191 --- /dev/null +++ b/day-20/src/part1.rs @@ -0,0 +1,230 @@ +use std::collections::{HashMap, HashSet, VecDeque}; + +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::{alpha1, line_ending}, + multi::separated_list1, + sequence::preceded, + IResult, +}; + +use crate::error::AocError; + +#[derive(Debug)] +enum State { + On, + Off, +} + +#[derive(Debug, Copy, Clone)] +enum Signal { + High, + Low, +} + +#[derive(Debug)] +enum ModuleType<'a> { + Broadcaster, + FlipFlop { state: State }, + Conjunction { inputs: HashMap<&'a str, Signal> }, + Test, +} + +#[derive(Debug)] +struct Module<'a> { + id: &'a str, + module_type: ModuleType<'a>, + outputs: Vec<&'a str>, +} + +impl<'a> Module<'a> { + #[tracing::instrument] + fn process(&mut self, from: &'a str, signal: &Signal) -> Vec<(&'a str, &'a str, Signal)> { + match (&mut self.module_type, signal) { + (ModuleType::Broadcaster, signal) => self + .outputs + .iter() + .map(|o| (self.id, *o, *signal)) + .collect(), + (ModuleType::FlipFlop { .. }, Signal::High) => { + vec![] + } + (ModuleType::FlipFlop { state: State::On }, Signal::Low) => { + self.module_type = ModuleType::FlipFlop { state: State::Off }; + self.outputs + .iter() + .map(|o| (self.id, *o, Signal::Low)) + .collect() + } + (ModuleType::FlipFlop { state: State::Off }, Signal::Low) => { + self.module_type = ModuleType::FlipFlop { state: State::On }; + self.outputs + .iter() + .map(|o| (self.id, *o, Signal::High)) + .collect() + } + (ModuleType::Conjunction { ref mut inputs }, pulse) => { + inputs.entry(from).and_modify(|s| *s = *pulse); + if inputs.values().all(|s| matches!(s, Signal::High)) { + self.outputs + .iter() + .map(|o| (self.id, *o, Signal::Low)) + .collect() + } else { + self.outputs + .iter() + .map(|o| (self.id, *o, Signal::High)) + .collect() + } + } + _ => { + vec![] + } + } + } +} + +fn broadcaster(input: &str) -> IResult<&str, Module> { + let (input, id) = alpha1(input)?; + let (input, _) = tag(" -> ")(input)?; + let (input, outputs) = separated_list1(tag(", "), alpha1)(input)?; + + Ok(( + input, + Module { + id, + module_type: ModuleType::Broadcaster, + outputs, + }, + )) +} + +fn flip_flop(input: &str) -> IResult<&str, Module> { + let (input, id) = preceded(nom::character::complete::char('%'), alpha1)(input)?; + let (input, _) = tag(" -> ")(input)?; + let (input, outputs) = separated_list1(tag(", "), alpha1)(input)?; + + Ok(( + input, + Module { + id, + module_type: ModuleType::FlipFlop { state: State::Off }, + outputs, + }, + )) +} + +fn conjunction(input: &str) -> IResult<&str, Module> { + let (input, id) = preceded(nom::character::complete::char('&'), alpha1)(input)?; + let (input, _) = tag(" -> ")(input)?; + let (input, outputs) = separated_list1(tag(", "), alpha1)(input)?; + + Ok(( + input, + Module { + id, + module_type: ModuleType::Conjunction { + inputs: HashMap::new(), + }, + outputs, + }, + )) +} + +fn parser(input: &str) -> IResult<&str, HashMap<&str, Module>> { + let (input, modules) = + separated_list1(line_ending, alt((broadcaster, flip_flop, conjunction)))(input)?; + + let mut module_map = modules + .into_iter() + .map(|m| (m.id, m)) + .collect::>(); + + let conjunctions: Vec<&str> = module_map + .iter() + .filter(|(_, m)| matches!(m.module_type, ModuleType::Conjunction { .. })) + .map(|(id, _)| *id) + .collect(); + + let all_inputs: HashSet<&str> = module_map.keys().copied().collect(); + let all_outputs: HashSet<&str> = module_map + .iter() + .flat_map(|(_, m)| m.outputs.iter().copied()) + .collect(); + for module in all_outputs.difference(&all_inputs) { + module_map.entry(module).or_insert(Module { + id: module, + module_type: ModuleType::Test, + outputs: Vec::default(), + }); + } + + for conjunction in conjunctions { + let inputs: Vec<&str> = module_map + .iter() + .filter(|(_, m)| m.outputs.contains(&conjunction)) + .map(|(id, _)| *id) + .collect(); + + for input in inputs { + module_map.entry(conjunction).and_modify(|m| { + if let ModuleType::Conjunction { inputs, .. } = &mut m.module_type { + inputs.insert(input, Signal::Low); + } + }); + } + } + + Ok((input, module_map)) +} + +#[tracing::instrument(skip(input))] +pub fn process(input: &str) -> miette::Result { + let (input, mut modules) = parser(input).unwrap(); + debug_assert!(input.is_empty()); + let mut low_signals = 0; + let mut high_signals = 0; + for _ in 0..1000 + // For each button press + { + let mut queue = VecDeque::new(); + queue.push_back(("button", "broadcaster", Signal::Low)); + low_signals += 1; // From initial button press + loop { + if queue.is_empty() { + break; + } + let (from, to, signal) = queue.pop_front().unwrap(); + tracing::info!("{} -{:?}-> {}", from, signal, to); + let module = modules.get_mut(to).unwrap(); + let signals = module.process(from, &signal); + signals.iter().for_each(|(_, _, s)| match s { + Signal::High => high_signals += 1, + Signal::Low => low_signals += 1, + }); + queue.extend(signals); + } + } + + Ok(low_signals * high_signals) +} + +#[cfg(test)] +mod tests { + use super::*; + + use rstest::rstest; + + #[test_log::test(rstest)] + #[case("test-input.txt", 32000000)] + #[case("test-input2.txt", 11687500)] + fn test_process(#[case] filename: &str, #[case] expected: u64) -> miette::Result<()> { + let input = + String::from_utf8_lossy(&std::fs::read(std::path::Path::new(filename)).unwrap()) + .parse::() + .unwrap(); + assert_eq!(expected, process(&input)?); + Ok(()) + } +} diff --git a/day-20/src/part2.rs b/day-20/src/part2.rs new file mode 100644 index 0000000..1ff3976 --- /dev/null +++ b/day-20/src/part2.rs @@ -0,0 +1,18 @@ +use crate::error::AocError; + +#[tracing::instrument] +pub fn process(input: &str) -> miette::Result { + Ok(0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test_log::test] + fn test_process() -> miette::Result<()> { + let input = include_str!("../test-input.txt"); + assert_eq!(0, process(input)?); + Ok(()) + } +} diff --git a/day-20/test-input.txt b/day-20/test-input.txt new file mode 100644 index 0000000..9ed10dd --- /dev/null +++ b/day-20/test-input.txt @@ -0,0 +1,5 @@ +broadcaster -> a, b, c +%a -> b +%b -> c +%c -> inv +&inv -> a \ No newline at end of file diff --git a/day-20/test-input2.txt b/day-20/test-input2.txt new file mode 100644 index 0000000..4da4379 --- /dev/null +++ b/day-20/test-input2.txt @@ -0,0 +1,5 @@ +broadcaster -> a +%a -> inv, con +&inv -> b +%b -> con +&con -> output \ No newline at end of file