diff --git a/2024/day24/Cargo.lock b/2024/day24/Cargo.lock new file mode 100644 index 0000000..78b8897 --- /dev/null +++ b/2024/day24/Cargo.lock @@ -0,0 +1,77 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "day24" +version = "0.1.0" +dependencies = [ + "itertools", + "lazy_static", + "regex", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" diff --git a/2024/day24/Cargo.toml b/2024/day24/Cargo.toml new file mode 100644 index 0000000..31711bf --- /dev/null +++ b/2024/day24/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "day24" +version = "0.1.0" +edition = "2021" + +[dependencies] +itertools = "0.13.0" +lazy_static = "1.5.0" +regex = "1.11.1" diff --git a/2024/day24/README.md b/2024/day24/README.md new file mode 100644 index 0000000..1de6518 --- /dev/null +++ b/2024/day24/README.md @@ -0,0 +1,25 @@ +# Solution Day 24 + +Logic gate. What a treat. I couldn't fully solve it, though, as I got caught in thinking _way_ too complex. See below... + +## Task 1 + +Rather straightforward. Each gate is represented by the `Gate` struct, which implements a `calculate` function, that either +gets the input values from the known inputs or recursivly calculates other gates to then calculate its own value. This +is slightly less efficient, than optimal, assuming we could cache intermediate values, but the calculation still is more +than fast enough. + +## Task 2 + +Didn't solve this one because I thought way too complex. Looking at the input size, it was clear, that a brute-force search +for the correct swaps wouldn't be possible. I tried a few approaches to limit the search space. My main idea was, that no +swaps should be done for outputs, that are already correct (since we might change those) and that swaps should only be performed +between gates with different output values (otherwise we wouldn't change anything). After implementing the necessary preparations, +I discovered, that the search space is _still_ too large (I just let the commented out loop generating all the possible swaps +run without actually performing the swap, just to get a feel and was bored quite quickly of the runtime). After that I gave up +and searched for inspiration on reddit. + +Turns out: my complex approach got me down the wrong path completely. If I had remembered my days as uni and the details +of what an adder looked like (I could still design one without looking it up. I just never thought of this circuit being +a standard parallel adder), I probably would've been able to find the swapped wires quite quickly. Well, you can't always +win, I guess… \ No newline at end of file diff --git a/2024/day24/src/main.rs b/2024/day24/src/main.rs new file mode 100644 index 0000000..f3edeb1 --- /dev/null +++ b/2024/day24/src/main.rs @@ -0,0 +1,244 @@ +use std::collections::{HashMap, HashSet}; + +use itertools::Itertools; +use lazy_static::lazy_static; +use regex::Regex; + +enum Op { + AND, + OR, + XOR, +} + +struct Gate { + input1_name: &'static str, + input2_name: &'static str, + output_name: &'static str, + op: Op, +} + +impl Gate { + fn calculate( + &self, + inputs: &HashMap<&'static str, bool>, + gates: &HashMap<&'static str, Gate>, + ) -> bool { + let input1 = inputs.get(self.input1_name).cloned().unwrap_or_else(|| { + gates + .get(self.input1_name) + .unwrap() + .calculate(inputs, gates) + }); + let input2 = inputs.get(self.input2_name).cloned().unwrap_or_else(|| { + gates + .get(self.input2_name) + .unwrap() + .calculate(inputs, gates) + }); + match self.op { + Op::AND => input1 && input2, + Op::OR => input1 || input2, + Op::XOR => input1 ^ input2, + } + } +} + +lazy_static! { + static ref GATE_REGEX: Regex = + Regex::new(r"([\w\d]+) (AND|OR|XOR) ([\w\d]+) -> ([\w\d]+)").unwrap(); +} + +impl From<&'static str> for Gate { + fn from(value: &'static str) -> Self { + let matches = GATE_REGEX.captures(value).unwrap(); + Gate { + input1_name: matches.get(1).unwrap().as_str(), + input2_name: matches.get(3).unwrap().as_str(), + output_name: matches.get(4).unwrap().as_str(), + op: match matches.get(2).unwrap().as_str() { + "AND" => Op::AND, + "OR" => Op::OR, + "XOR" => Op::XOR, + op => panic!("Unknown op {op}"), + }, + } + } +} + +struct Input { + inputs: HashMap<&'static str, bool>, + gates: HashMap<&'static str, Gate>, +} + +fn load_input() -> Input { + let mut data = include_str!("../input.txt").lines(); + + Input { + inputs: HashMap::from_iter( + data.by_ref() + .take_while(|line| !line.is_empty()) + .map(|line| { + let mut parts = line.split(": "); + ( + parts.next().unwrap(), + if parts.next().unwrap() == "1" { + true + } else { + false + }, + ) + }), + ), + gates: HashMap::from_iter(data.map(|line| { + let gate: Gate = line.into(); + (gate.output_name, gate) + })), + } +} + +fn to_decimal>(bits: Iter) -> u64 { + bits.enumerate().fold(0_u64, |existing, (idx, value)| { + if value { + existing + (1_u64 << idx) + } else { + existing + } + }) +} + +fn task1() { + let input = load_input(); + + let mut outputs: Vec<_> = input + .gates + .iter() + .filter(|(&output_name, _)| output_name.starts_with("z")) + .map(|(_, gate)| gate) + .collect(); + + outputs.sort_by_key(|gate| gate.output_name); + + let result = outputs + .iter() + .map(|gate| gate.calculate(&input.inputs, &input.gates)); + + let number = to_decimal(result); + + println!("Task: result: {number}"); +} + +fn get_affecting_outputs( + output: &str, + gates: &HashMap<&'static str, Gate>, +) -> HashSet<&'static str> { + let mut affecting_outputs = HashSet::new(); + let gate = gates.get(output).unwrap(); // we know it exists + if !gate.input1_name.starts_with("x") && !gate.input1_name.starts_with("y") { + affecting_outputs.insert(gate.input1_name); + // affecting_outputs.(&mut ); + get_affecting_outputs(gate.input1_name, gates) + .into_iter() + .for_each(|gate| { + affecting_outputs.insert(gate); + }); + } + if !gate.input2_name.starts_with("x") && !gate.input2_name.starts_with("y") { + affecting_outputs.insert(gate.input2_name); + get_affecting_outputs(gate.input2_name, gates) + .into_iter() + .for_each(|gate| { + affecting_outputs.insert(gate); + }); + } + affecting_outputs +} + +fn task2() { + let input = load_input(); + + let all_output_names: Vec<_> = input + .gates + .iter() + .map(|(_, gate)| gate.output_name) + .collect(); + + let mut x_inputs: Vec<_> = input + .inputs + .iter() + .filter(|(&inp, _)| inp.starts_with("x")) + .collect(); + x_inputs.sort_by_key(|(&inp, _)| inp); + let x_value = to_decimal(x_inputs.iter().map(|(_, &value)| value)); + let mut y_inputs: Vec<_> = input + .inputs + .iter() + .filter(|(&inp, _)| inp.starts_with("y")) + .collect(); + y_inputs.sort_by_key(|(&inp, _)| inp); + let y_value = to_decimal(x_inputs.iter().map(|(_, &value)| value)); + + let target_value = x_value + y_value; + + let mut outputs: Vec<_> = input + .gates + .iter() + .filter(|(&output_name, _)| output_name.starts_with("z")) + .map(|(_, gate)| gate) + .collect(); + outputs.sort_by_key(|gate| gate.output_name); + + let result = outputs + .iter() + .map(|gate| gate.calculate(&input.inputs, &input.gates)); + + let number = to_decimal(result); + + let wrong_outputs: Vec<_> = (0..outputs.len()) + .filter(|idx| number & (1 << idx) != target_value & (1 << idx)) + .map(|idx| format!("z{idx:#02}")) + .collect(); + let correct_outputs: Vec<_> = (0..outputs.len()) + .filter(|idx| number & (1 << idx) == target_value & (1 << idx)) + .map(|idx| format!("z{idx:#02}")) + .collect(); + + let potential_bad_gates: Vec<_> = wrong_outputs + .iter() + .map(|output| get_affecting_outputs(output, &input.gates)) + .collect(); + let good_gates: Vec<_> = correct_outputs + .iter() + .map(|output| get_affecting_outputs(output, &input.gates)) + .collect(); + // let's find all gates, that influence wrong output bits, but _NOT_ right output bits + let only_bad_gates: HashSet<_> = potential_bad_gates + .iter() + .flat_map(|block| { + block + .iter() + .filter(|&gate| good_gates.iter().any(|block| block.contains(gate))) + }) + .collect(); + + let gate_values: HashMap<&str, bool> = + HashMap::from_iter(input.gates.iter().map(|(&output_name, gate)| { + (output_name, gate.calculate(&input.inputs, &input.gates)) + })); + + // separate all bad gates into true and false because it makes no sense to switch two gates with an identical value + let (false_gates, true_gates): (Vec<_>, Vec<_>) = gate_values + .iter() + .filter(|(&name, _)| only_bad_gates.contains(&name)) + .partition(|(_, &value)| value); + + // for replacement_false_block in false_gates.iter().combinations(4) { + // for replacement_true_block in true_gates.iter().permutations(4) { + // TODO execute the swap and test -> this is too slow. See README for explanation + // } + // } +} + +fn main() { + task1(); + task2(); +}