-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathaoc201723.py
178 lines (133 loc) · 4.33 KB
/
aoc201723.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
"""AoC 23, 2017: Coprocessor Conflagration."""
# Standard library imports
import collections
import math
import pathlib
import sys
from dataclasses import dataclass
def parse_data(puzzle_input):
"""Parse input."""
return [parse_instruction(line) for line in puzzle_input.split("\n")]
def parse_instruction(line):
"""Parse one instruction.
## Examples:
>>> parse_instruction("snd g")
('snd', 'g')
>>> parse_instruction("set g 45")
('set', 'g', 45)
>>> parse_instruction("mul z a")
('mul', 'z', 'a')
"""
instruction, register, *args = line.split()
return instruction, try_int(register), *[try_int(arg) for arg in args]
def try_int(maybe_number):
"""Convert to int if possible.
## Examples:
>>> try_int("42")
42
>>> try_int("g")
'g'
"""
try:
return int(maybe_number)
except ValueError:
return maybe_number
def part1(instructions):
"""Solve part 1."""
program = Program(instructions, debug=True)
program.run()
return program.num_multiplications
def part2(instructions):
"""Solve part 2.
The given program calculates the number of non-primes in a given range. Find
the range, then calculate the non-primes with a more effective sieve.
"""
step = -instructions[30][2]
# Run set up to find start and end values, then jump out of program
instructions[8] = ("jnz", 1, len(instructions))
program = Program(instructions, debug=False)
program.run()
start, end = program.registers["b"], program.registers["c"]
# Calculate non-primes
return non_primes(start, end, step)
def non_primes(start, end, step):
"""Find the number of non-primes in the given interval.
## Example:
20, 26, 32, 35, 38, 44, 50, 56, 62, 65, 68, 74, 77, 80, 86, 92, 95, 98
>>> non_primes(20, 100, 3)
18
"""
divisors = primes(math.isqrt(end) + 1)
num_non_primes = 0
for num in range(start, end + 1, step):
for divisor in divisors:
if num % divisor == 0:
num_non_primes += 1
break
return num_non_primes
def primes(end):
"""Find the primes up to end.
## Example:
>>> primes(50)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
"""
divisors = [2, 3]
for num in range(5, end + 1):
for divisor in divisors:
if num % divisor == 0:
break
else:
divisors.append(num)
return divisors
class Registers(collections.UserDict):
"""Dictionary with special handling of missing keys."""
def __missing__(self, key):
"""Pass through integer keys. Otherwise default to 0.
## Examples:
>>> Registers({"a": 1})["a"]
1
>>> Registers({"a": 1})["b"]
0
>>> Registers({"a": 1})[14]
14
"""
if isinstance(key, int):
return key
self.data[key] = 0
return 0
@dataclass
class Program:
"""A program that can be run."""
instructions: list[tuple[str, str, int]]
num_multiplications: int = 0
debug: bool = False
def __post_init__(self):
"""Set up registers."""
self.num_instructions = len(self.instructions)
self.registers = Registers({"a": 0 if self.debug else 1})
def run(self):
"""Run program to termination."""
pointer = 0
while 0 <= pointer < self.num_instructions:
instruction, register, *args = self.instructions[pointer]
value = self.registers[args[0]] if args else 0
if instruction == "set":
self.registers[register] = value
elif instruction == "sub":
self.registers[register] -= value
elif instruction == "mul":
self.registers[register] *= value
self.num_multiplications += 1
elif instruction == "jnz" and self.registers[register] != 0:
pointer += value - 1
pointer += 1
def solve(puzzle_input):
"""Solve the puzzle for the given input."""
data = parse_data(puzzle_input)
yield part1(data)
yield part2(data)
if __name__ == "__main__":
for path in sys.argv[1:]:
print(f"\n{path}:")
solutions = solve(puzzle_input=pathlib.Path(path).read_text().rstrip())
print("\n".join(str(solution) for solution in solutions))