Metadata
-
Date
-
Tagged
-
Part of series
- Advent of Code 2024 Day 1
- Advent of Code 2024 Day 2
- Advent of Code 2024 Day 3
- Advent of Code 2024 Day 4
- Advent of Code 2024 Day 5
- Advent of Code 2024 Day 6
- Advent of Code 2024 Day 7
- Advent of Code 2024 Day 8
- Advent of Code 2024 Day 9
- Advent of Code 2024 Day 10
- Advent of Code 2024 Day 11
- Advent of Code 2024 Day 12
- Advent of Code 2024 Day 13
- Advent of Code 2024 Day 14
- Advent of Code 2024 Day 15
- Advent of Code 2024 Day 16
- Advent of Code 2024 Day 17
- Advent of Code 2024 Day 18
- Advent of Code 2024 Day 19
- Advent of Code 2024 Day 20
- Advent of Code 2024 Day 21
- Advent of Code 2024 Day 22
- Advent of Code 2024 Day 23
- Advent of Code 2024 Day 24
- Advent of Code 2024 Day 25
-
Older post
-
Newer post
Advent of Code 2024 Day 13
Day 13: Claw Contraption
https://adventofcode.com/2024/day/13
Another day, another familiar location.
Today’s input is the behaviour of a bunch of arcade claw machines.
An example input looks like this:
Button A: X+94, Y+34Button B: X+22, Y+67Prize: X=8400, Y=5400
Button A: X+26, Y+66Button B: X+67, Y+21Prize: X=12748, Y=12176
Button A: X+17, Y+86Button B: X+84, Y+37Prize: X=7870, Y=6450
Button A: X+69, Y+23Button B: X+27, Y+71Prize: X=18641, Y=10279
Each block of input represents one machine. Their grippers all move on a 2D-plane directly above the prize.
Each machine has 2 buttons, which each move the gripper some direction in the x
and y
axis.
The 3 lines per machine tell you:
- What pressing A does to the gripper coordinates
- What pressing B does to the gripper coordinates
- The coordinates of the prize
The goal is to place the gripper directly over the prize. You do this by pressing the A and B button a number of times.
Parsing
If you’ve been following along, you know what I do when I see a puzzle with coordinates by now, Point
!
Point
#[derive(PartialEq, Eq)]struct Point { x: i64, y: i64,}
impl Point { fn new(x: i64, y: i64) -> Self { Self { x, y } }}
I stored each machine as a 3-item array of Point
s.
use itertools::Itertools;
fn parse(input: &str) -> Vec<[Point; 3]> { let mut machines = Vec::new(); for block in input.split("\n\n") { let (adx, ady, bdx, bdy, x, y) = block .split(|c: char| !c.is_ascii_digit()) .filter(|s| !s.is_empty()) .map(|s| s.parse().unwrap()) .collect_tuple() .unwrap(); let a = Point::new(adx, ady); let b = Point::new(bdx, bdy); let prize = Point::new(x, y); machines.push([a, b, prize]); } machines}
Part 1
- Pressing the A button costs 3 tokens.
- Pressing the B button costs 1 token.
Don’t overthink why, the answer is ✨reasons✨.
The question asks for sum of the lowest cost for all winnable machines.
This is a system of 2 linear equations.
Variable definitions:
- The amount of times button A is pressed
- The amount of times button B is pressed
- The change in
x
coordinate pressing A causes - The change in
y
coordinate pressing A causes - The change in
x
coordinate pressing B causes - The change in
y
coordinate pressing B causes - The
x
coordinate of the prize - The
y
coordinate of the prize
The 2 equations describe which x
and y
coordinate the gripper has after n
presses.
This location should be equal to the location of the prize to win:
Solving for first. By multiplying both sides of each equation so the term cancels out when I subtract the 2 equations from eachother:
- the top is multiplied by
- the bottom is multiplied by
Isolating as that is the only unknown, all other variables can be replaced by numbers.
After calculating the amount of times A should be pressed, calculate the amount of times B should be pressed. I did this by replacing by its value in one of the original functions.
Now that we have the amount of times to press the A button and the amount of times to press the B button, we need to check if those numbers make sense. After all, when dealing with 2 linear equations there are 3 possibilities:
- They intersect once
- They are parallel and never intersect
- They represent the same line, overlap, so intersect infinitely many times
Luckily, this question never runs in to option 2 or 3. Because the puzzlemaster decided to be kind today.
But there is still an extra option I have to handle, I’m not magic and can not press a button a fractional amount of times. (bummer, I know)
So in the code, I check if the position the gripper reaches after A presses and B presses is the same as the position of the prize.
If it is, I return a successful result with a price of .
Helpers
Coding that math into a function.
It returns something when the found intersection of the 2 lines is the same as the location of the prize.
If the intersection exists, but is not reachable in whole button presses, the function returns nothing.
fn solve(a: Point, b: Point, prize: Point) -> Option<i64> { // lines are never parallel in this question assert!(((a.y as f64 / a.x as f64) - (b.y as f64 / b.x as f64)).abs() > f64::EPSILON);
let na = ((prize.x * b.y) - (prize.y * b.x)) / ((a.x * b.y) - (a.y * b.x)); let nb = (prize.x - na * a.x) / b.x; let solution = Point::new(na * a.x + nb * b.x, na * a.y + nb * b.y); (solution == prize).then_some(3 * na + nb)}
Code
A short function because of the helper functions again.
- Parse the input into a list of
[A, B, Prize]
points. - Solve using that combination of
Point
s - If solveable: add the solution cost to the sum
fn part_1(input: &str) -> i64 { parse(input) .into_iter() .filter_map(|[a, b, prize]| solve(a, b, prize)) .sum()}
Part 2
Woopsie, small mistake. The prize coordinates are both (x and y) 10 trillion less than what they should have been.
const TEN_TRILLY: i64 = 10_000_000_000_000;pub fn part_2(input: &str) -> i64 { parse(input) .into_iter() .filter_map(|[a, b, mut prize]| { prize.x += TEN_TRILLY; prize.y += TEN_TRILLY; solve(a, b, prize) }) .sum()}
Final code
use itertools::Itertools;
#[derive(PartialEq, Eq)]struct Point { x: i64, y: i64,}
impl Point { fn new(x: i64, y: i64) -> Self { Self { x, y } }}
fn parse(input: &str) -> Vec<[Point; 3]> { let mut machines = Vec::new(); for block in input.split("\n\n") { let (adx, ady, bdx, bdy, x, y) = block .split(|c: char| !c.is_ascii_digit()) .filter(|s| !s.is_empty()) .map(|s| s.parse().unwrap()) .collect_tuple() .unwrap(); let a = Point::new(adx, ady); let b = Point::new(bdx, bdy); let prize = Point::new(x, y); machines.push([a, b, prize]); } machines}
fn solve(a: Point, b: Point, prize: Point) -> Option<i64> { // lines are never parallel in this question assert!((a.y as f64 / a.x as f64) != (b.y as f64 / b.x as f64));
let na = ((prize.x * b.y) - (prize.y * b.x)) / ((a.x * b.y) - (a.y * b.x)); let nb = (prize.x - na * a.x) / b.x; let solution = Point::new(na * a.x + nb * b.x, na * a.y + nb * b.y); (solution == prize).then_some(3 * na + nb)}
pub fn part_1(input: &str) -> i64 { parse(input) .into_iter() .filter_map(|[a, b, prize]| solve(a, b, prize)) .sum()}
const TEN_TRILLY: i64 = 10_000_000_000_000;pub fn part_2(input: &str) -> i64 { parse(input) .into_iter() .filter_map(|[a, b, mut prize]| { prize.x += TEN_TRILLY; prize.y += TEN_TRILLY; solve(a, b, prize) }) .sum()}