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