-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathday21.py
158 lines (135 loc) · 5 KB
/
day21.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
from collections import defaultdict, Counter
from itertools import combinations, product
def day21(inp, part1=True):
codes = inp.splitlines()
door_map = {
(0, 0): 'A',
(0, -1): '0',
(-1, 0): '3',
(-1, -1): '2',
(-1, -2): '1',
(-2, 0): '6',
(-2, -1): '5',
(-2, -2): '4',
(-3, 0): '9',
(-3, -1): '8',
(-3, -2): '7',
}
robot_map = {
(0, 0): 'A',
(0, -1): '^',
(1, 0): '>',
(1, -1): 'v',
(1, -2): '<',
}
door_transfer = get_transitions(door_map)
robot_transfer = get_transitions(robot_map)
if part1:
robot_transfer_count = 2
else:
robot_transfer_count = 25
# only keep relevant door transfers
final_door_transfer = {}
for code in codes:
for start_key, end_key in zip('A' + code, code):
final_door_transfer[start_key, end_key] = door_transfer[start_key, end_key]
door_transfer = final_door_transfer
result = 0
for code in codes:
# for each code generate all possible binary path choices
lowest_complexity = float('inf')
for door_transfer_values in product(*door_transfer.values()):
door_transfer_now = dict(zip(door_transfer, door_transfer_values))
for robot_transfer_values in product(*robot_transfer.values()):
robot_transfer_now = dict(zip(robot_transfer, robot_transfer_values))
transfers = [door_transfer_now] + [robot_transfer_now] * robot_transfer_count
path_counts = find_path_counts(code, transfers)
path_length = sum(path_counts.values()) # fencepost minus implicit starting A
complexity = int(code.rstrip('A')) * path_length
if complexity < lowest_complexity:
lowest_complexity = complexity
result += lowest_complexity
return result
def get_transitions(coords):
"""Generate potential robot arm move instructions for any two buttons.
Parameters
----------
coords : dict[tuple, str]
Key coordinates, keys are positions and values are the key
label.
Returns
-------
dict[tuple, list]
Mapping from (start key, end key) tuples to a list of potential
shortest moves from the start key to the end key. The final 'A'
key is not included in the strings.
"""
deltas = {
'v': (1, 0),
'^': (-1, 0),
'>': (0, 1),
'<': (0, -1),
}
reverses = dict(zip('v^><', '^v<>'))
transfer = defaultdict(list)
for start, end in combinations(coords, 2):
start_key = coords[start]
end_key = coords[end]
dx = end[0] - start[0]
dy = end[1] - start[1]
vertical_dir = 'v' if dx > 0 else '^'
horizontal_dir = '>' if dy > 0 else '<'
vertical_part = vertical_dir * abs(dx)
horizontal_part = horizontal_dir * abs(dy)
paths = {vertical_part + horizontal_part, horizontal_part + vertical_part}
if len(paths) == 1:
# no-brainer
path = paths.pop()
transfer[start_key, end_key].append(path)
transfer[end_key, start_key].append(''.join([reverses[c] for c in path[::-1]]))
continue
# check if any of the two options are invalid
for path in paths:
pos = start
for step_dir in path:
delta = deltas[step_dir]
pos = pos[0] + delta[0], pos[1] + delta[1]
if pos not in coords:
break
else:
transfer[start_key, end_key].append(path)
transfer[end_key, start_key].append(''.join([reverses[c] for c in path[::-1]]))
return dict(transfer)
def find_path_counts(code, transfers):
"""Generate a code through a series of keyboard transitions.
Parameters
----------
code : str
The final target.
transfers : list[dict]
A list of transitions from `get_transitions()` corresponding to
the order in which the paths have to be encoded.
Returns
-------
collections.Counter
The path section frequencies on the first keypad (corresponding
to the last transfer mapping).
"""
section_counts = Counter()
target = code
for section in zip('A' + code[:-1], code):
section_counts[section] += 1
for i, transfer in enumerate(transfers):
next_section_counts = Counter()
for section, multiplicity in section_counts.items():
# watch out for missing (thing, thing) pairs
subpath = transfer.get(section, '') + 'A'
for next_section in zip('A' + subpath, subpath):
next_section_counts[next_section] += multiplicity
section_counts = next_section_counts
return section_counts
if __name__ == "__main__":
testinp = open('day21.testinp').read()
print(day21(testinp), day21(testinp, part1=False))
inp = open('day21.inp').read()
print(day21(inp), day21(inp, part1=False))