-
Notifications
You must be signed in to change notification settings - Fork 0
/
day19.py
139 lines (122 loc) · 5.31 KB
/
day19.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
from collections import defaultdict
import json
import math
def day19(inp):
workflows_block, ratings_block = inp.strip().split('\n\n')
parts = [
json.loads(rating.replace('x=', '"x":').replace('m=', '"m":').replace('a=', '"a":').replace('s=', '"s":'))
for rating in ratings_block.splitlines()
]
workflows_part1 = {}
workflows_part2 = {}
for workflow_str in workflows_block.splitlines():
label, _, code = workflow_str[:-1].partition('{')
steps = code.split(',')
def workflow_part1(part, steps=steps):
"""Take a specific part and return output label.
Parameters
----------
part : dict
Dict with keys in 'xmas' and integer values.
Returns
-------
str
The label (or verdict) as the output of this workflow.
"""
for step in steps:
if ':' not in step:
# 'A' or 'R'
return step
condition, _, result = step.partition(':')
if eval(f'part["{condition[0]}"]{condition[1:]}', {'part': part}):
return result
workflows_part1[label] = workflow_part1
def workflow_part2(part_ranges, steps=steps):
"""Take a part range specification and return output labels and ranges.
Parameters
----------
part_ranges : dict
Dict with keys in 'xmas' and (from, to) integer ranges as values.
Returns
-------
dict[list[dict]]
For each output label a list of dicts like ``part_ranges`` specifying
what falls there. (Sometimes labels, especially 'R' and 'A', repeat
within the same workflow.)
"""
part_ranges = part_ranges.copy()
output = defaultdict(list)
for step in steps:
if ':' not in step:
# if we're here we need to forward what's left of part_ranges
output[step].append(part_ranges)
continue
condition, _, result = step.partition(':')
feature, relation = condition[:2]
edge = int(condition[2:])
feature_range = part_ranges[feature]
if relation == '<':
if feature_range[0] >= edge:
# no value is inside
continue
forwarded_range = feature_range[0], min(feature_range[1], edge)
forwarded_part_ranges = part_ranges.copy()
forwarded_part_ranges[feature] = forwarded_range
output[result].append(forwarded_part_ranges)
# check if we have anything left outside to continue
if feature_range[1] >= edge + 1:
part_ranges[feature] = edge, feature_range[1]
continue
# otherwise no need to continue logic
break
if relation == '>':
if feature_range[1] <= edge + 1:
# no value is inside
continue
forwarded_range = max(feature_range[0], edge + 1), feature_range[1]
forwarded_part_ranges = part_ranges.copy()
forwarded_part_ranges[feature] = forwarded_range
output[result].append(forwarded_part_ranges)
# check if we have anything left outside to continue
if feature_range[0] <= edge:
part_ranges[feature] = feature_range[0], edge + 1
continue
# otherwise no need to continue logic
break
return output
workflows_part2[label] = workflow_part2
part1 = 0
for part in parts:
workflow_label = 'in'
while True:
workflow_label = workflows_part1[workflow_label](part)
if workflow_label in 'RA':
verdict = workflow_label
break
if verdict == 'R':
continue
part1 += sum(part.values())
part2 = 0
part_ranges_by_workflow = defaultdict(list)
part_ranges_by_workflow['in'].append(dict.fromkeys('xmas', (1, 4001)))
while any(lst for lst in part_ranges_by_workflow.values()):
workflow_label, part_ranges_for_label = next(item for item in part_ranges_by_workflow.items() if item[1])
part_ranges = part_ranges_for_label.pop()
for next_workflow_label, new_part_ranges_lst in workflows_part2[workflow_label](part_ranges).items():
if next_workflow_label == 'R':
continue
if next_workflow_label == 'A':
part2 += sum(
math.prod(
len(range(*edges)) for edges in new_part_ranges.values()
)
for new_part_ranges in new_part_ranges_lst
)
continue
part_ranges_by_workflow[next_workflow_label].extend(new_part_ranges_lst)
return part1, part2
if __name__ == "__main__":
testinp = open('day19.testinp').read()
print(*day19(testinp))
inp = open('day19.inp').read()
print(*day19(inp))