advent-of-code/2022/python/day16.py

159 lines
4.9 KiB
Python
Raw Normal View History

2022-12-16 06:13:24 +00:00
import sys
import matrix
import shared
import scanf
from dataclasses import dataclass
2022-12-17 03:07:17 +00:00
from collections import defaultdict
2022-12-16 06:13:24 +00:00
from typing import List, Dict
from pprint import pprint
2022-12-17 03:07:17 +00:00
import networkx as nx
2022-12-16 06:13:24 +00:00
2022-12-17 03:07:17 +00:00
from IPython.display import Image, display
2022-12-16 06:13:24 +00:00
@dataclass
class Valve:
label: str
rate: int
tunnels: List[str]
opened_at: int = -1
2022-12-17 03:07:17 +00:00
potential: Dict[str, int] = None
2022-12-16 06:13:24 +00:00
def set_potential(self, valves):
self.potential = {}
for tunnel in self.tunnels:
self.potential[tunnel] = valves[tunnel].rate
def highest_potential(self):
return max(self.potential, key=self.potential.get)
2022-12-17 03:07:17 +00:00
2022-12-16 06:13:24 +00:00
def parse(rows):
valves = {}
for row in rows:
2022-12-16 06:19:30 +00:00
left, right = row.split(" valve")
right = right.replace("s ", "").lstrip()
2022-12-16 06:13:24 +00:00
valve, rate = scanf.scanf("Valve %s has flow rate=%d; %*s %*s to", left)
2022-12-16 06:19:30 +00:00
tunnels = right.split(", ")
2022-12-17 03:07:17 +00:00
valves[valve] = Valve(label=valve, rate=rate, tunnels=tunnels)
2022-12-16 06:13:24 +00:00
2022-12-17 03:07:17 +00:00
for _, v in valves.items():
2022-12-16 06:13:24 +00:00
v.set_potential(valves)
return valves
def part1(rows, sample=False):
2022-12-17 03:07:17 +00:00
p1 = Part1(rows, sample, 30)
2022-12-16 06:13:24 +00:00
p1.run()
class Part1:
def __init__(self, rows, sample, minutes):
self.rows = rows
self.sample = sample
self.valves = parse(rows)
2022-12-17 03:07:17 +00:00
self.nonzero = {v.label: v for _, v in self.valves.items() if v.rate > 0}
2022-12-16 06:13:24 +00:00
self.cur = self.valves["AA"]
self.tick = 1
self.minutes = minutes
2022-12-17 03:07:17 +00:00
self.g = nx.DiGraph()
self.path_distances = defaultdict(dict)
self.set_up_graph()
def draw(self):
pdot = nx.drawing.nx_pydot.to_pydot(self.g)
pdot.write_png("15.png")
def set_up_graph(self):
for lbl, v in self.valves.items():
for t in v.tunnels:
# self.g.add_edge(lbl, t, {'weight':self.valves[t].rate})
self.g.add_edge(lbl, t, weight=self.valves[t].rate)
all_keys = self.valves.keys()
for lbl, _ in self.valves.items():
for other in all_keys:
if other == lbl:
continue
self.path_distances[lbl][other] = min([len(x) for x in nx.shortest_simple_paths(self.g, lbl, other)])
2022-12-16 06:13:24 +00:00
def do_tick(self, minute):
2022-12-17 03:07:17 +00:00
pressure = 0
2022-12-16 06:13:24 +00:00
opened = []
for _, valve in self.valves.items():
if valve.opened_at > 0:
2022-12-17 03:07:17 +00:00
pressure += valve.rate
opened.append(valve.label)
print(f"== Min {minute}:: Valves {', '.join(opened)} are open, releasing {pressure} pressure")
2022-12-16 06:13:24 +00:00
def calculate_total_flow(self):
total = 0
for label, valve in self.valves.items():
2022-12-17 03:07:17 +00:00
if valve.opened_at > 0:
2022-12-16 06:13:24 +00:00
total += valve.rate * (30 - valve.opened_at)
return total
def run(self):
2022-12-17 03:07:17 +00:00
# Construct the graph with vertices & edges from the input
# Call a function to compute the distances between every pair of vertices
# Create a closed set containing all the valves with non-zero rates
# At each step, iterate over the remaining set of closed, non-zero valves
# - Subtract the distance from remaining minutes
# - Calculate the flow (rate * remaining minutes)
# - Remove the recently opened valve from the closed set (functionally), so the deeper levels won't consider it
def priority(remaining):
_pris = []
for _,n in self.nonzero.items():
# (time_remaining - distance_to_valve - 1) * flow rate
pri = (remaining - self.path_distances[self.cur.label][n.label] - 1) * n.rate
_pris.append((n.label, pri))
_pris = list(sorted(_pris, key=lambda x: x[1], reverse=True))
return _pris
remaining = self.minutes
open_order = []
while len(self.nonzero):
if remaining <= 0:
print("ran out of time")
break
self.do_tick(31-remaining)
pris = priority(remaining)
print(pris)
_pri, _ = pris.pop(0)
n = self.nonzero[_pri]
del self.nonzero[_pri]
open_order.append(n.label)
print("\tMoving to", n.label)
print("\tOpening ", n.label)
distance = self.path_distances[self.cur.label][n.label]
self.cur = n
self.cur.opened_at = self.minutes - (remaining + 1)
remaining -= distance # Move
remaining -= 1 # open
print(remaining)
print(open_order)
print("sample: 1651")
print("total flow:", self.calculate_total_flow())
2022-12-16 06:13:24 +00:00
def main():
sample = False
if sys.argv[-1] == "--sample":
sample = True
rows = [row for row in shared.load_rows(16)]
with shared.elapsed_timer() as elapsed:
part1(rows, sample)
print("🕒", elapsed())
2022-12-17 03:07:17 +00:00
# with shared.elapsed_timer() as elapsed:
2022-12-16 06:13:24 +00:00
# part2(rows, sample)
# print("🕒", elapsed())
if __name__ == "__main__":
main()