From ce9038a90e334bc79041d5bd5016a2c7cd8fa08b Mon Sep 17 00:00:00 2001 From: Markus Brueckner Date: Sat, 7 Dec 2024 21:02:24 +0100 Subject: [PATCH] day 7 --- 2024/day7/Cargo.lock | 7 ++ 2024/day7/Cargo.toml | 4 ++ 2024/day7/README.md | 34 ++++++++++ 2024/day7/src/main.rs | 145 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+) create mode 100644 2024/day7/Cargo.lock create mode 100644 2024/day7/Cargo.toml create mode 100644 2024/day7/README.md create mode 100644 2024/day7/src/main.rs diff --git a/2024/day7/Cargo.lock b/2024/day7/Cargo.lock new file mode 100644 index 0000000..7af9b93 --- /dev/null +++ b/2024/day7/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day7" +version = "0.1.0" diff --git a/2024/day7/Cargo.toml b/2024/day7/Cargo.toml new file mode 100644 index 0000000..81924dc --- /dev/null +++ b/2024/day7/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "day7" +version = "0.1.0" +edition = "2021" diff --git a/2024/day7/README.md b/2024/day7/README.md new file mode 100644 index 0000000..f2f6143 --- /dev/null +++ b/2024/day7/README.md @@ -0,0 +1,34 @@ +# Solution Day 7 + +Calculating expression and testing whether we can make them equal on both sides. Not too hard. +Parsing the input is rather easy with a bit of `.split()` and `.parse()`. Algorithm for testing the +expressions is of the brute-force variety: generate all possible combinations of operators, calculate +the expression for each combination and see if it matches the result. + +## Task 1 + +This one was easy at first: all possible combinations of operators (since there are only 2) can be generated +by counting between `0` and `pow(2, size)` (where `size` is the number of operators we need to fill the equation) +and treating each number as a bit mask, where the bit in position `n` represents an operator (`Add` if the bit +is `0`, `Multiply` if it is `1`). This was easy enough to implement and I even got it right on the first try. + +## Task 2 + +I kind of had the inkling, that my simple "bitmask" solution for task 1 would get me into trouble with task 2. And +wouldn't you know? It did. Three operators cannot be easily expressed with a binary number (more on that in a bit). +I thought about implementing symbolic calculations (i.e. representing the number as `Vec` and doing the +counting by hand, including overflow and everything), but that seemed too much effort. The crate `itertools` contains +a `.combinations()` method, which can be made to work like this: `[Add, Add, Multiply, Multiply, Concat, Concat].combinations(2)` +(not the actual syntax, but you get the idea). This will generate all possible length 2 combination of the operators. +Works, but is _exceedingly_ slow, so I abandoned that approach. + +My actual solution goes back to my teaching days at uni: calculating the digits of a number in arbitrary base (in +this case: base 3). To get the next digit at the back, calculate `number % base`. Then continue with `number /= base` to +get the number without the last digit and continue until you run out of digits (or, in my case: until you've generate enough +digits corresponding to the operators needed). + +Another tricky bit was the implementation of the `||` operator. Since the whole implementation is done numerically, it +_should_ be implemented as `left * pow(10, round(log10(right))) + right` (basically: multiply with 10 ^ number of right digits to +get enough 0s at the end and add the number on top). While this works in theory, there is probably a precision or something, +since `ceil` and `log10` only work in `f64` instead of `u64`. I consistently got the wrong results. In the end I gave up and +used a combination of `format!()` and `.parse()` to concatenate in string-space. \ No newline at end of file diff --git a/2024/day7/src/main.rs b/2024/day7/src/main.rs new file mode 100644 index 0000000..dd2c64e --- /dev/null +++ b/2024/day7/src/main.rs @@ -0,0 +1,145 @@ +#[derive(Debug)] +struct Expression { + result: u64, + operands: Vec, +} + +impl Expression { + fn calculate(&self, operators: &Vec) -> u64 { + let mut result = self.operands[0]; + for idx in 0..operators.len() { + match operators[idx] { + Ops::Add => result += self.operands[idx + 1], + Ops::Multiply => result *= self.operands[idx + 1], + Ops::Concat => { + result = format!("{result}{}", self.operands[idx + 1]) + .parse() + .expect("Concat result must be parsable as a number") + } + } + } + result + } +} + +impl From<&str> for Expression { + fn from(value: &str) -> Self { + let mut parts = value.split(":"); + let result = parts + .next() + .expect("Expected result before :") + .parse() + .expect("Expected result to be number"); + let operands: Vec = parts + .next() + .expect("Expected operands after :") + .split(" ") + .filter_map(|op| op.parse().ok()) + .collect(); + return Self { result, operands }; + } +} + +#[derive(Debug, Clone)] +enum Ops { + Add, + Multiply, + Concat, +} + +#[derive(Debug)] +struct OperatorGenerator { + size: u32, + base: usize, + current_combination: usize, +} + +impl OperatorGenerator { + fn new(size: u32, base: usize) -> OperatorGenerator { + OperatorGenerator { + size, + base, + current_combination: 0, + } + } +} + +impl Iterator for OperatorGenerator { + type Item = Vec; + + fn next(&mut self) -> Option { + if self.current_combination == usize::pow(self.base, self.size) { + None + } else { + let mut current = self.current_combination; + self.current_combination += 1; + let mut ops = vec![]; + for idx in 0..self.size { + let remainder = current % self.base; + current /= self.base; + match remainder { + 0 => ops.push(Ops::Add), + 1 => ops.push(Ops::Multiply), + 2 => ops.push(Ops::Concat), + _ => panic!("Don't support more than 3 operators"), + } + } + Some(ops) + } + } +} + +fn read_input() -> Vec { + let input = std::fs::read_to_string("./input.txt").unwrap(); + input + .lines() + .filter(|line| line.len() > 0) + .map(|line| line.into()) + .collect() +} + +fn test_expression(exp: &Expression, with_concat: bool) -> bool { + let generator = OperatorGenerator::new( + exp.operands.len() as u32 - 1, + if with_concat { 3 } else { 2 }, + ); + for operators in generator { + if exp.calculate(&operators) == exp.result { + return true; + } + } + false +} + +fn task1() { + let expressions = read_input(); + + let sum = expressions.iter().fold(0, |sum, exp| { + if test_expression(exp, false) { + sum + exp.result + } else { + sum + } + }); + + println!("Task 1: Sum: {sum}"); +} + +fn task2() { + let expressions = read_input(); + + let sum = expressions.iter().fold(0, |sum, exp| { + if test_expression(exp, true) { + sum + exp.result + } else { + sum + } + }); + + println!("Task 2: Sum: {sum}"); +} + +fn main() { + task1(); + task2(); +}