This commit is contained in:
Markus Brueckner 2024-12-07 21:02:24 +01:00
parent c44da97678
commit ce9038a90e
4 changed files with 190 additions and 0 deletions

7
2024/day7/Cargo.lock generated Normal file
View file

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

4
2024/day7/Cargo.toml Normal file
View file

@ -0,0 +1,4 @@
[package]
name = "day7"
version = "0.1.0"
edition = "2021"

34
2024/day7/README.md Normal file
View file

@ -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<usize>` 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.

145
2024/day7/src/main.rs Normal file
View file

@ -0,0 +1,145 @@
#[derive(Debug)]
struct Expression {
result: u64,
operands: Vec<u64>,
}
impl Expression {
fn calculate(&self, operators: &Vec<Ops>) -> 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<u64> = 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<Ops>;
fn next(&mut self) -> Option<Self::Item> {
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<Expression> {
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();
}