Add 2024 Day 13

This commit is contained in:
Burnus 2024-12-14 10:14:28 +01:00
parent fe30d53314
commit 8d69ea0cd5
5 changed files with 1527 additions and 0 deletions

View file

@ -0,0 +1,13 @@
[package]
name = "day13_claw_contraption"
version = "0.1.0"
edition = "2021"
[dependencies]
[dev-dependencies]
# criterion = "0.5.1"
[[bench]]
name = "test_benchmark"
harness = false

View file

@ -0,0 +1,90 @@
Next up: the [lobby](/2020/day/24) of a resort on a tropical island. The Historians take a moment to admire the hexagonal floor tiles before spreading out.
Fortunately, it looks like the resort has a new [arcade](https://en.wikipedia.org/wiki/Amusement_arcade)! Maybe you can win some prizes from the [claw machines](https://en.wikipedia.org/wiki/Claw_machine)?
The claw machines here are a little unusual. Instead of a joystick or directional buttons to control the claw, these machines have two buttons labeled `A` and `B`. Worse, you can't just put in a token and play; it costs *3 tokens* to push the `A` button and *1 token* to push the `B` button.
With a little experimentation, you figure out that each machine's buttons are configured to move the claw a specific amount to the *right* (along the `X` axis) and a specific amount *forward* (along the `Y` axis) each time that button is pressed.
Each machine contains one *prize*; to win the prize, the claw must be positioned *exactly* above the prize on both the `X` and `Y` axes.
You wonder: what is the smallest number of tokens you would have to spend to win as many prizes as possible? You assemble a list of every machine's button behavior and prize location (your puzzle input). For example:
```
Button A: X+94, Y+34
Button B: X+22, Y+67
Prize: X=8400, Y=5400
Button A: X+26, Y+66
Button B: X+67, Y+21
Prize: X=12748, Y=12176
Button A: X+17, Y+86
Button B: X+84, Y+37
Prize: X=7870, Y=6450
Button A: X+69, Y+23
Button B: X+27, Y+71
Prize: X=18641, Y=10279
```
This list describes the button configuration and prize location of four different claw machines.
For now, consider just the first claw machine in the list:
* Pushing the machine's `A` button would move the claw `94` units along the `X` axis and `34` units along the `Y` axis.
* Pushing the `B` button would move the claw `22` units along the `X` axis and `67` units along the `Y` axis.
* The prize is located at `X=8400`, `Y=5400`; this means that from the claw's initial position, it would need to move exactly `8400` units along the `X` axis and exactly `5400` units along the `Y` axis to be perfectly aligned with the prize in this machine.
The cheapest way to win the prize is by pushing the `A` button `80` times and the `B` button `40` times. This would line up the claw along the `X` axis (because `80*94 + 40*22 = 8400`) and along the `Y` axis (because `80*34 + 40*67 = 5400`). Doing this would cost `80*3` tokens for the `A` presses and `40*1` for the `B` presses, a total of `*280*` tokens.
For the second and fourth claw machines, there is no combination of A and B presses that will ever win a prize.
For the third claw machine, the cheapest way to win the prize is by pushing the `A` button `38` times and the `B` button `86` times. Doing this would cost a total of `*200*` tokens.
So, the most prizes you could possibly win is two; the minimum tokens you would have to spend to win all (two) prizes is `*480*`.
You estimate that each button would need to be pressed *no more than `100` times* to win a prize. How else would someone be expected to play?
Figure out how to win as many prizes as possible. *What is the fewest tokens you would have to spend to win all possible prizes?*
Your puzzle answer was `29598`.
\--- Part Two ---
----------
As you go to win the first prize, you discover that the claw is nowhere near where you expected it would be. Due to a unit conversion error in your measurements, the position of every prize is actually `10000000000000` higher on both the `X` and `Y` axis!
Add `10000000000000` to the `X` and `Y` position of every prize. After making this change, the example above would now look like this:
```
Button A: X+94, Y+34
Button B: X+22, Y+67
Prize: X=10000000008400, Y=10000000005400
Button A: X+26, Y+66
Button B: X+67, Y+21
Prize: X=10000000012748, Y=10000000012176
Button A: X+17, Y+86
Button B: X+84, Y+37
Prize: X=10000000007870, Y=10000000006450
Button A: X+69, Y+23
Button B: X+27, Y+71
Prize: X=10000000018641, Y=10000000010279
```
Now, it is only possible to win a prize on the second and fourth claw machines. Unfortunately, it will take *many more than `100` presses* to do so.
Using the corrected prize coordinates, figure out how to win as many prizes as possible. *What is the fewest tokens you would have to spend to win all possible prizes?*
Your puzzle answer was `93217456941970`.
Both parts of this puzzle are complete! They provide two gold stars: \*\*
At this point, you should [return to your Advent calendar](/2024) and try another puzzle.
If you still want to see it, you can [get your puzzle input](13/input).

View file

@ -0,0 +1,130 @@
use core::fmt::Display;
use std::num::ParseIntError;
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError<'a> {
ParseIntError(std::num::ParseIntError),
InputMalformed(&'a str),
LineMalformed(&'a str),
}
impl From<ParseIntError> for ParseError<'_> {
fn from(value: ParseIntError) -> Self {
Self::ParseIntError(value)
}
}
impl Display for ParseError<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InputMalformed(v) => write!(f, "Machine is malformed: {v}"),
Self::LineMalformed(v) => write!(f, "Line is malformed: {v}"),
Self::ParseIntError(e) => write!(f, "Unable to parse into integer: {e}"),
}
}
}
type Coordinates = (usize, usize);
struct Machine {
btn_a: Coordinates,
btn_b: Coordinates,
prize: Coordinates,
}
impl<'a> TryFrom<&'a str> for Machine {
type Error = ParseError<'a>;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
let lines: Vec<_> = value.lines().collect();
if lines.len() != 3 {
return Err(Self::Error::InputMalformed(value));
}
let a: Vec<_> = lines[0].split(&['+', ',']).collect();
if a.len() != 4 {
return Err(Self::Error::LineMalformed(lines[0]));
}
let btn_a = (a[1].parse()?, a[3].parse()?);
let b: Vec<_> = lines[1].split(&['+', ',']).collect();
if b.len() != 4 {
return Err(Self::Error::LineMalformed(lines[1]));
}
let btn_b = (b[1].parse()?, b[3].parse()?);
let p: Vec<_> = lines[2].split(&['=', ',']).collect();
if p.len() != 4 {
return Err(Self::Error::LineMalformed(lines[2]));
}
let prize = (p[1].parse()?, p[3].parse()?);
Ok(Self {
btn_a,
btn_b,
prize,
})
}
}
impl Machine {
fn prize_cost(&self) -> Option<usize> {
// Determine the winning combination using matrix inversion. This method delivers the only
// combination that will work (provided the button vectors are lenearly independent), but
// the result may be non-integer, in which case there is no valid solution.
let inverse_determinant = 1.0 / ((self.btn_a.0*self.btn_b.1) as f64 - (self.btn_a.1*self.btn_b.0) as f64);
let a_presses = (inverse_determinant * (
(self.btn_b.1 * self.prize.0) as f64 - (self.btn_b.0 * self.prize.1) as f64)
).round() as isize;
let b_presses = (inverse_determinant * (
(self.btn_a.0 * self.prize.1) as f64 - (self.btn_a.1 * self.prize.0) as f64)
).round() as isize;
// Try all neighbouring combinations to account for rounding errors
for delta_a in (-1..=1).filter(|da| a_presses + da >= 0) {
let a = (a_presses + delta_a) as usize;
for delta_b in (-1..=1).filter(|db| b_presses + db >= 0) {
let b = (b_presses + delta_b) as usize;
if a * self.btn_a.0 + b * self.btn_b.0 == self.prize.0 &&
a * self.btn_a.1 + b * self.btn_b.1 == self.prize.1 {
return Some(3 * a + b)
}
}
}
None
}
}
pub fn run(input: &str) -> Result<(usize, usize), ParseError> {
let mut machines: Vec<_> = input.split("\n\n").map(Machine::try_from).collect::<Result<Vec<_>, _>>()?;
let first = machines.iter().filter_map(|m| m.prize_cost()).sum();
machines.iter_mut().for_each(|m| {
m.prize.0 += 10000000000000;
m.prize.1 += 10000000000000;
});
let second = machines.iter().filter_map(|m| m.prize_cost()).sum();
Ok((first, second))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::read_to_string;
fn read_file(name: &str) -> String {
read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string()
}
#[test]
fn test_sample() {
let sample_input = read_file("tests/sample_input");
// The result for part 2 is not given in the challenge, but is what my solution finds.
// Thus, it may be incorrect (although I doubt it since the other 3 results are correct).
assert_eq!(run(&sample_input), Ok((480, 875318608908)));
}
#[test]
fn test_challenge() {
let challenge_input = read_file("tests/challenge_input");
assert_eq!(run(&challenge_input), Ok((29598, 93217456941970)));
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
Button A: X+94, Y+34
Button B: X+22, Y+67
Prize: X=8400, Y=5400
Button A: X+26, Y+66
Button B: X+67, Y+21
Prize: X=12748, Y=12176
Button A: X+17, Y+86
Button B: X+84, Y+37
Prize: X=7870, Y=6450
Button A: X+69, Y+23
Button B: X+27, Y+71
Prize: X=18641, Y=10279