Skip to content

Commit 785497d

Browse files
committed
Day 13
1 parent 32e0c06 commit 785497d

File tree

4 files changed

+213
-0
lines changed

4 files changed

+213
-0
lines changed

Cargo.lock

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ edition = "2021"
99
clap = { version = "4.4", features = ["derive"] }
1010
itertools = "0.14.0"
1111
rayon = "1.8"
12+
static_assertions = "1.1.0"
1213
tailcall = "1.0.1"
14+
thiserror = "2.0.12"

src/day13.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
use std::{iter::from_fn, num::ParseIntError, str::FromStr};
2+
use thiserror::Error;
3+
4+
type StoreInt = u64;
5+
type CalcInt = i128;
6+
7+
const PART2_SHIFT: StoreInt = 10000000000000;
8+
9+
const_assert!(u64::MAX > 2 * PART2_SHIFT);
10+
const_assert!(i128::MAX > 4 * PART2_SHIFT as CalcInt * PART2_SHIFT as CalcInt);
11+
12+
pub fn day13(input: &str) -> (usize, usize) {
13+
let machines = Machine::parse_all(input)
14+
.filter_map(|m| m.ok())
15+
.collect::<Vec<_>>();
16+
17+
(part1(&machines), part2(&machines))
18+
}
19+
20+
fn part1(machines: &[Machine]) -> usize {
21+
machines
22+
.iter()
23+
.filter_map(|m| m.solve())
24+
.map(|s| s.cost())
25+
.sum()
26+
}
27+
28+
fn part2(machines: &[Machine]) -> usize {
29+
machines
30+
.iter()
31+
.map(|m| Machine {
32+
prize: m.prize.map(|x| x + PART2_SHIFT),
33+
a_action: m.a_action,
34+
b_action: m.b_action,
35+
})
36+
.filter_map(|m| m.solve())
37+
.map(|s| s.cost())
38+
.sum()
39+
}
40+
41+
#[derive(Debug, PartialEq, Eq)]
42+
struct Machine {
43+
prize: [StoreInt; 2],
44+
a_action: [StoreInt; 2],
45+
b_action: [StoreInt; 2],
46+
}
47+
48+
#[derive(Error, Debug)]
49+
enum MachineParseError {
50+
#[error("Wrong Format")]
51+
WrongFormat(#[from] ParseIntError),
52+
#[error("Not enough Data")]
53+
NotEnoughData,
54+
}
55+
56+
impl Machine {
57+
fn solve(&self) -> Option<Solution> {
58+
// prize = a_presses * a_action + b_presses * b_action
59+
// is a 2x2 LSE, use Cramer's rule to solve
60+
let [p1, p2] = self.prize.map(|x| x as CalcInt);
61+
let [a1, a2] = self.a_action.map(|x| x as CalcInt);
62+
let [b1, b2] = self.b_action.map(|x| x as CalcInt);
63+
64+
let det = a1 * b2 - a2 * b1;
65+
66+
if det == 0 {
67+
return None;
68+
}
69+
70+
let (a_presses, a_rem) = divmod(b2 * p1 - b1 * p2, det);
71+
let (b_presses, b_rem) = divmod(a1 * p2 - a2 * p1, det);
72+
73+
if a_presses < 0 || a_rem != 0 || b_presses < 0 || b_rem != 0 {
74+
return None;
75+
}
76+
77+
return StoreInt::try_from(a_presses)
78+
.ok()
79+
.zip_with(StoreInt::try_from(b_presses).ok(), Solution::new);
80+
81+
fn divmod(x: CalcInt, y: CalcInt) -> (CalcInt, CalcInt) {
82+
(x / y, x % y)
83+
}
84+
}
85+
}
86+
87+
impl FromStr for Machine {
88+
type Err = MachineParseError;
89+
90+
fn from_str(s: &str) -> Result<Self, Self::Err> {
91+
fn parse_button(line: &str, name: &str) -> Result<(StoreInt, StoreInt), MachineParseError> {
92+
line.strip_prefix(&format!("Button {}: X+", name))
93+
.and_then(|rest| rest.split_once(", Y+"))
94+
.ok_or(MachineParseError::NotEnoughData) // Would want WrongFormat, but then the typechecker gets confused
95+
.and_then(|(x, y)| Ok((x.parse()?, y.parse()?)))
96+
}
97+
98+
fn parse_prize(line: &str) -> Result<(StoreInt, StoreInt), MachineParseError> {
99+
line.strip_prefix("Prize: X=")
100+
.and_then(|rest| rest.split_once(", Y="))
101+
.ok_or(MachineParseError::NotEnoughData)
102+
.and_then(|(x, y)| Ok((x.parse()?, y.parse()?)))
103+
}
104+
105+
let mut lines = s.lines();
106+
let a_line = lines.next().ok_or(MachineParseError::NotEnoughData)?;
107+
let b_line = lines.next().ok_or(MachineParseError::NotEnoughData)?;
108+
let p_line = lines.next().ok_or(MachineParseError::NotEnoughData)?;
109+
110+
let (a_x, a_y) = parse_button(a_line, "A")?;
111+
let (b_x, b_y) = parse_button(b_line, "B")?;
112+
let (p_x, p_y) = parse_prize(p_line)?;
113+
114+
Ok(Machine {
115+
prize: [p_x, p_y],
116+
a_action: [a_x, a_y],
117+
b_action: [b_x, b_y],
118+
})
119+
}
120+
}
121+
122+
trait ParseIncrementally: FromStr {
123+
const NUM_LINES: usize;
124+
125+
fn incremental_parse(s: &str) -> (Result<Self, Self::Err>, &str) {
126+
fn split_n_newlines(n: usize, s: &str) -> (&str, &str) {
127+
if n == 0 {
128+
return ("", &s);
129+
}
130+
let newlines: Vec<_> = s.match_indices('\n').take(n).collect();
131+
if newlines.len() == n {
132+
let split_pos = newlines[n - 1].0;
133+
(&s[..split_pos], &s[split_pos + 1..])
134+
} else {
135+
(&s, "")
136+
}
137+
}
138+
139+
let (start, rest) = split_n_newlines(Self::NUM_LINES, s);
140+
(start.parse(), rest)
141+
}
142+
143+
fn parse_all(s: &str) -> impl Iterator<Item = Result<Self, Self::Err>> {
144+
let mut rem = s;
145+
from_fn(move || {
146+
if rem.is_empty() {
147+
return None;
148+
}
149+
let (result, rest) = Self::incremental_parse(rem);
150+
rem = rest;
151+
Some(result)
152+
})
153+
}
154+
}
155+
156+
impl ParseIncrementally for Machine {
157+
const NUM_LINES: usize = 4;
158+
}
159+
160+
#[derive(Debug, PartialEq, Eq)]
161+
struct Solution {
162+
a_presses: StoreInt,
163+
b_presses: StoreInt,
164+
}
165+
166+
impl Solution {
167+
fn new(a_presses: StoreInt, b_presses: StoreInt) -> Self {
168+
Self {
169+
a_presses,
170+
b_presses,
171+
}
172+
}
173+
174+
fn cost(&self) -> usize {
175+
self.a_presses as usize * 3 + self.b_presses as usize * 1
176+
}
177+
}

src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
#![feature(cmp_minmax)]
2+
#![feature(option_zip)]
3+
4+
#[macro_use]
5+
extern crate static_assertions;
26

37
use std::{fmt::Debug, fs, time::Instant};
48

@@ -16,6 +20,7 @@ mod day09;
1620
mod day10;
1721
mod day11;
1822
mod day12;
23+
mod day13;
1924

2025
#[derive(Parser)]
2126
struct Args {
@@ -77,6 +82,7 @@ fn main() {
7782
(day10::day10, "day10"),
7883
(day11::day11, "day11"),
7984
(day12::day12, "day12"),
85+
(day13::day13, "day13"),
8086
];
8187

8288
// underflow is fine

0 commit comments

Comments
 (0)