This commit is contained in:
Markus Brueckner 2024-12-24 23:17:44 +01:00
parent b83e13d0bb
commit 87883a52dc
4 changed files with 355 additions and 0 deletions

77
2024/day24/Cargo.lock generated Normal file
View file

@ -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"

9
2024/day24/Cargo.toml Normal file
View file

@ -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"

25
2024/day24/README.md Normal file
View file

@ -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…

244
2024/day24/src/main.rs Normal file
View file

@ -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<Iter: Iterator<Item = bool>>(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();
}