-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathday16.py
168 lines (128 loc) · 4.4 KB
/
day16.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
from day import Day
from aocd import submit
from functools import cache
from collections import abc, deque
from dataclasses import dataclass
class FrozenDict(abc.Mapping):
"""A hashable dictionary that is immutable once created."""
def __init__(self, *args, **kwargs):
self._d = dict(*args, **kwargs)
self._hash = None
def __repr__(self):
return str(self._d)
def __iter__(self):
return iter(self._d)
def __len__(self):
return len(self._d)
def __delitem__(self, key):
del self._d[key]
self._hash = None
def pop(self, key, default=None):
self._hash = None
return self._d.pop(key, default)
def __getitem__(self, key):
return self._d[key]
def __setitem__(self, key, value):
self._d[key] = value
def __hash__(self):
# It would have been simpler and maybe more obvious to
# use hash(tuple(sorted(self._d.iteritems()))) from this discussion
# so far, but this solution is O(n). I don't know what kind of
# n we are going to run into, but sometimes it's hard to resist the
# urge to optimize when it will gain improved algorithmic performance.
if self._hash is None:
hash_ = 0
for pair in self.items():
hash_ ^= hash(pair)
self._hash = hash_
return self._hash
@dataclass
class Valve:
name: str
flow: int
tunnels: tuple
neighbours: FrozenDict = None
hash: int = None
def __repr__(self):
return f"""Valve {self.name} has
flow rate={self.flow};
tunnels lead to valves {', '.join(self.tunnels)},
with shortest paths: {self.neighbours}\n"""
def __len__(self):
return len(self.tunnels)
def __hash__(self):
return hash((self.name, self.flow, self.tunnels))
def parse(data):
valves = FrozenDict()
for line in data:
name, flow, tunnel = list(line)
flow = int(flow)
tunnels = tuple(tunnel.split(", "))
valves[name] = Valve(name, flow, tunnels)
return valves
def find_shortest_path(valves, start, end):
# BFS
visited = set()
queue = deque([(start, 0)])
while queue:
current, steps = queue.popleft()
if current == end:
return steps
visited.add(current)
for tunnel in valves[current].tunnels:
if tunnel not in visited:
queue.append((tunnel, steps + 1))
def compress_valves(valves):
# Find all valves with flow
flow_valves = [valve.name for valve in valves.values() if valve.flow != 0]
# find the shortest path between all valves with flow
for v in flow_valves + ["AA"]:
valves[v].neighbours = FrozenDict()
for v2 in flow_valves:
if v != v2:
valves[v].neighbours[v2] = find_shortest_path(valves, v, v2)
for v in flow_valves:
if valves[v].flow == 0 and not v == "AA":
valves.pop(v)
return valves
@cache
def tunneling(valves, opened=frozenset(), valve="AA", minutes=30, ele=False):
if minutes <= 0:
return 0
if len(opened) == len(valves) - 1:
return 0
# First let the elephant run thanks to the cache function
max_steam = 0 if not ele else tunneling(valves, opened, valve="AA", minutes=26)
minutes -= 1
current_steam = minutes * valves[valve].flow if valve not in opened else 0
opened = frozenset(tuple(opened) + (valve,))
# Now test all the neighbouring valves
for tunnel, steps in valves[valve].neighbours.items():
max_steam = max(
max_steam,
current_steam
+ tunneling(valves, opened, valve=tunnel, minutes=minutes - steps + (current_steam == 0), ele=ele),
)
return max_steam
def main(day, part=1):
regex = r"Valve ([A-Z]+)[a-z ]+\=(-?\d+)\;[a-z ]+([A-Z, ]+)"
day.parse_regex(regex, typing=(str, int, str))
day.data = parse(day.data)
day.data = compress_valves(day.data)
if part == 1:
# return day.data
return tunneling(day.data)
if part == 2:
# return elephant_tunneling(day.data)
return tunneling(day.data, minutes=26, ele=True)
if __name__ == "__main__":
day = Day(16)
day.download()
day.load()
p1 = main(day)
print(p1)
submit(p1, part="a", day=16, year=2022)
day.load()
p2 = main(day, part=2)
print(p2)
submit(p2, part="b", day=16, year=2022)