Skip to content

Commit 8d62389

Browse files
committed
Day 17 solutions, that took ages...
1 parent 3514a04 commit 8d62389

File tree

5 files changed

+217
-3
lines changed

5 files changed

+217
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,4 @@ This should start the server at `localhost:8080`.
102102
❄️ [Day 14](aoc-solver/src/y2024/day14.rs)
103103
❄️ [Day 15](aoc-solver/src/y2024/day15.rs)
104104
❄️ [Day 16](aoc-solver/src/y2024/day16.rs)
105+
❄️ [Day 17](aoc-solver/src/y2024/day17.rs)

aoc-solver/src/y2024/day17.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
use itertools::Itertools;
2+
3+
use crate::solution::{AocError, Solution};
4+
5+
#[derive(Clone, Eq, PartialEq)]
6+
struct Computer {
7+
a: u64,
8+
b: u64,
9+
c: u64,
10+
instruction_pointer: usize,
11+
program: Vec<u8>,
12+
}
13+
14+
fn parse(input: &str) -> Result<Computer, AocError> {
15+
let (registers, program) = input
16+
.split_once("\n\n")
17+
.ok_or_else(|| AocError::parse(input, "Missing register or program sections"))?;
18+
19+
let (a, b, c) = registers
20+
.lines()
21+
.collect_tuple()
22+
.ok_or(AocError::parse(registers, "Missing position"))?;
23+
24+
let a = a
25+
.strip_prefix("Register A: ")
26+
.and_then(|a| a.parse::<u64>().ok())
27+
.ok_or_else(|| AocError::parse(a, "A"))?;
28+
29+
let b = b
30+
.strip_prefix("Register B: ")
31+
.and_then(|b| b.parse::<u64>().ok())
32+
.ok_or_else(|| AocError::parse(b, "B"))?;
33+
let c = c
34+
.strip_prefix("Register C: ")
35+
.and_then(|c| c.parse::<u64>().ok())
36+
.ok_or_else(|| AocError::parse(c, "C"))?;
37+
38+
let program = program
39+
.strip_prefix("Program: ")
40+
.ok_or_else(|| AocError::parse(program, "Program"))?;
41+
42+
let program = program
43+
.split(",")
44+
.map(|instruction| {
45+
instruction
46+
.parse::<u8>()
47+
.map_err(|err| AocError::parse(instruction, err))
48+
})
49+
.try_collect()?;
50+
51+
Ok(Computer {
52+
a,
53+
b,
54+
c,
55+
program,
56+
instruction_pointer: 0,
57+
})
58+
}
59+
60+
fn resolve_combo(operand: &u8, computer: &Computer) -> u64 {
61+
match operand {
62+
0..=3 => *operand as u64,
63+
4 => computer.a,
64+
5 => computer.b,
65+
6 => computer.c,
66+
_ => unreachable!(),
67+
}
68+
}
69+
70+
fn run_program(mut computer: Computer) -> Vec<u8> {
71+
let mut output = vec![];
72+
73+
while let (Some(opcode), Some(operand)) = (
74+
computer.program.get(computer.instruction_pointer),
75+
computer.program.get(computer.instruction_pointer + 1),
76+
) {
77+
computer.instruction_pointer += 2;
78+
79+
match opcode {
80+
// `adv`, performs division of A register by two to the power of combo operand.
81+
0 => computer.a >>= resolve_combo(operand, &computer),
82+
// `bxl`, bitwise XOR of register B and the instruction's literal operand.
83+
1 => computer.b ^= *operand as u64,
84+
// `bst`, combo operand modulo 8 stored to B.
85+
2 => computer.b = resolve_combo(operand, &computer) % 8,
86+
// `jnz`, jumps instruction pointer if A is not zero to literal operand.
87+
3 if computer.a != 0 => computer.instruction_pointer = *operand as usize,
88+
// `bxc`, bitwise XOR of register B and register C to register B.
89+
4 => computer.b ^= computer.c,
90+
// `out`, combo operand modulo 8, then outputs that value.
91+
5 => output.push((resolve_combo(operand, &computer) & 0b111) as u8),
92+
// `bdv`, performs division of A register by two to the power of combo operand to register B.
93+
6 => computer.b = computer.a >> resolve_combo(operand, &computer),
94+
// `cdv`, performs division of A register by two to the power of combo operand to register C.
95+
7 => computer.c = computer.a >> resolve_combo(operand, &computer),
96+
_ => {}
97+
}
98+
}
99+
100+
output
101+
}
102+
103+
pub struct Day17;
104+
impl Solution for Day17 {
105+
type A = String;
106+
type B = u64;
107+
108+
fn default_input(&self) -> &'static str {
109+
include_str!("../../../inputs/2024/day17.txt")
110+
}
111+
112+
fn part_1(&self, input: &str) -> Result<String, AocError> {
113+
let computer = parse(input)?;
114+
let output = run_program(computer);
115+
116+
Ok(output.into_iter().join(","))
117+
}
118+
119+
fn part_2(&self, input: &str) -> Result<u64, AocError> {
120+
let Computer { program, .. } = parse(input)?;
121+
122+
// This approach works on my (and the example) input, which has "0,3" as the
123+
// only instruction affecting A's value, dividing A by 8 and "3,0" as the last
124+
// instruction to halt the program when A equals zero, otherwise looping back to beginning.
125+
// This is the only loop in the instructions, and also conveniently B and C values are never
126+
// persisted between these loops.
127+
//
128+
// Knowing this, since we're expected to output 16 digits we can estimate that the loop has to run through
129+
// 16 times and A's initial value is somewhere between 8^15 and 8^16,
130+
// but those are too large numbers for efficient brute forcing.
131+
//
132+
// If we look for the expected instructions one by one starting from the end of the list,
133+
// at the time the last input is being output A has been divided by 8 (A >> 3)
134+
// so many times that there are only last three bits left. If we try all possible
135+
// values for these last bits (0b000 ..= 0b111) and see which ones produce the expected
136+
// instruction output, save all those, shift A left by three bits (A << 3) and try all of the
137+
// next three bit combinations to see which produce the second instruction we should be able to
138+
// eventually construct the full initial A value.
139+
140+
let mut possible = vec![0];
141+
142+
for instruction in program.iter().rev() {
143+
let mut next_possible = Vec::new();
144+
145+
for prev_a in possible {
146+
for last_3_bits in 0b000..=0b111 {
147+
let a = (prev_a << 3) + last_3_bits;
148+
let output = run_program(Computer {
149+
a,
150+
b: 0,
151+
c: 0,
152+
instruction_pointer: 0,
153+
program: program.clone(),
154+
});
155+
156+
if let Some(produced) = output.first() {
157+
if produced == instruction {
158+
next_possible.push(a);
159+
}
160+
}
161+
}
162+
}
163+
164+
possible = next_possible;
165+
}
166+
167+
let initial_a = possible
168+
.into_iter()
169+
.min()
170+
.ok_or(AocError::logic("No solution"))?;
171+
172+
Ok(initial_a)
173+
}
174+
}
175+
176+
#[cfg(test)]
177+
mod tests {
178+
use super::*;
179+
180+
#[test]
181+
fn it_solves_part1_example_1() {
182+
assert_eq!(
183+
Day17.part_1(
184+
"Register A: 729\n\
185+
Register B: 0\n\
186+
Register C: 0\n\
187+
\n\
188+
Program: 0,1,5,4,3,0"
189+
),
190+
Ok(String::from("4,6,3,5,6,3,5,2,1,0"))
191+
);
192+
}
193+
194+
#[test]
195+
fn it_solves_part2_example_1() {
196+
assert_eq!(
197+
Day17.part_2(
198+
"Register A: 2024\n\
199+
Register B: 0\n\
200+
Register C: 0\n\
201+
\n\
202+
Program: 0,3,5,4,3,0"
203+
),
204+
Ok(117440)
205+
);
206+
}
207+
}

aoc-solver/src/y2024/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub mod day13;
1616
pub mod day14;
1717
pub mod day15;
1818
pub mod day16;
19-
// pub mod day17;
19+
pub mod day17;
2020
// pub mod day18;
2121
// pub mod day19;
2222
// pub mod day20;
@@ -26,7 +26,7 @@ pub mod day16;
2626
// pub mod day24;
2727
// pub mod day25;
2828

29-
pub const MAX_DAYS: u8 = 16;
29+
pub const MAX_DAYS: u8 = 17;
3030

3131
pub struct Y2024;
3232

@@ -49,7 +49,7 @@ impl Solver for Y2024 {
4949
14 => day14::Day14.run(input, 14, 2024),
5050
15 => day15::Day15.run(input, 15, 2024),
5151
16 => day16::Day16.run(input, 16, 2024),
52-
// 17 => day17::Day17.run(input, 17, 2024),
52+
17 => day17::Day17.run(input, 17, 2024),
5353
// 18 => day18::Day18.run(input, 18, 2024),
5454
// 19 => day19::Day19.run(input, 19, 2024),
5555
// 20 => day20::Day20.run(input, 20, 2024),

aoc-web/src/header.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub fn header(props: &HeaderProps) -> Html {
3737
<NavLink route={Route::Solution { year: 2024, day: 15 }} current={props.route.clone()} text={"15"}/>
3838
<NavLink route={Route::WarehouseRobot} current={props.route.clone()} text={"15+"}/>
3939
<NavLink route={Route::Solution { year: 2024, day: 16 }} current={props.route.clone()} text={"16"}/>
40+
<NavLink route={Route::Solution { year: 2024, day: 17 }} current={props.route.clone()} text={"17"}/>
4041
</>
4142
}
4243
},

inputs/2024/day17.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Register A: 27575648
2+
Register B: 0
3+
Register C: 0
4+
5+
Program: 2,4,1,2,7,5,4,1,1,3,5,5,0,3,3,0

0 commit comments

Comments
 (0)