day 7
This commit is contained in:
parent
c44da97678
commit
ce9038a90e
4 changed files with 190 additions and 0 deletions
7
2024/day7/Cargo.lock
generated
Normal file
7
2024/day7/Cargo.lock
generated
Normal 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
4
2024/day7/Cargo.toml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[package]
|
||||||
|
name = "day7"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
34
2024/day7/README.md
Normal file
34
2024/day7/README.md
Normal 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
145
2024/day7/src/main.rs
Normal 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();
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue