diff --git a/advent/days/day16/__init__.py b/advent/days/day16/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/advent/days/day16/data/example01.txt b/advent/days/day16/data/example01.txt new file mode 100644 index 0000000..85fa5b0 --- /dev/null +++ b/advent/days/day16/data/example01.txt @@ -0,0 +1,10 @@ +Valve AA has flow rate=0; tunnels lead to valves DD, II, BB +Valve BB has flow rate=13; tunnels lead to valves CC, AA +Valve CC has flow rate=2; tunnels lead to valves DD, BB +Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE +Valve EE has flow rate=3; tunnels lead to valves FF, DD +Valve FF has flow rate=0; tunnels lead to valves EE, GG +Valve GG has flow rate=0; tunnels lead to valves FF, HH +Valve HH has flow rate=22; tunnel leads to valve GG +Valve II has flow rate=0; tunnels lead to valves AA, JJ +Valve JJ has flow rate=21; tunnel leads to valve II \ No newline at end of file diff --git a/advent/days/day16/data/input.txt b/advent/days/day16/data/input.txt new file mode 100644 index 0000000..044d0db --- /dev/null +++ b/advent/days/day16/data/input.txt @@ -0,0 +1,59 @@ +Valve SY has flow rate=0; tunnels lead to valves GW, LW +Valve TS has flow rate=0; tunnels lead to valves CC, OP +Valve LU has flow rate=0; tunnels lead to valves PS, XJ +Valve ND has flow rate=0; tunnels lead to valves EN, TL +Valve PD has flow rate=0; tunnels lead to valves TL, LI +Valve VF has flow rate=0; tunnels lead to valves LW, RX +Valve LD has flow rate=0; tunnels lead to valves AD, LP +Valve DG has flow rate=0; tunnels lead to valves DR, SS +Valve IG has flow rate=8; tunnels lead to valves AN, YA, GA +Valve LK has flow rate=0; tunnels lead to valves HQ, LW +Valve TD has flow rate=14; tunnels lead to valves BG, CQ +Valve CQ has flow rate=0; tunnels lead to valves TD, HD +Valve AZ has flow rate=0; tunnels lead to valves AD, XW +Valve ZU has flow rate=0; tunnels lead to valves TL, AN +Valve HD has flow rate=0; tunnels lead to valves BP, CQ +Valve FX has flow rate=0; tunnels lead to valves LW, XM +Valve CU has flow rate=18; tunnels lead to valves BX, VA, RX, DF +Valve SS has flow rate=17; tunnels lead to valves DG, ZD, ZG +Valve BP has flow rate=19; tunnels lead to valves HD, ZD +Valve DZ has flow rate=0; tunnels lead to valves XS, CC +Valve PS has flow rate=0; tunnels lead to valves GH, LU +Valve TA has flow rate=0; tunnels lead to valves LI, AA +Valve BG has flow rate=0; tunnels lead to valves TD, ZG +Valve WP has flow rate=0; tunnels lead to valves OB, AA +Valve XS has flow rate=9; tunnels lead to valves EN, DZ +Valve AA has flow rate=0; tunnels lead to valves WG, GA, VO, WP, TA +Valve LW has flow rate=25; tunnels lead to valves LK, FX, SY, VF +Valve AD has flow rate=23; tunnels lead to valves DF, GW, AZ, LD, FM +Valve EN has flow rate=0; tunnels lead to valves ND, XS +Valve ZG has flow rate=0; tunnels lead to valves SS, BG +Valve LI has flow rate=11; tunnels lead to valves YA, XM, TA, PD +Valve VO has flow rate=0; tunnels lead to valves AA, OD +Valve AN has flow rate=0; tunnels lead to valves IG, ZU +Valve GH has flow rate=15; tunnels lead to valves VA, PS +Valve OP has flow rate=4; tunnels lead to valves AJ, TS, FM, BX, NM +Valve BX has flow rate=0; tunnels lead to valves OP, CU +Valve RX has flow rate=0; tunnels lead to valves CU, VF +Valve FM has flow rate=0; tunnels lead to valves OP, AD +Valve OB has flow rate=0; tunnels lead to valves WP, XW +Valve CC has flow rate=3; tunnels lead to valves QS, LP, DZ, OD, TS +Valve LP has flow rate=0; tunnels lead to valves LD, CC +Valve NM has flow rate=0; tunnels lead to valves WH, OP +Valve HQ has flow rate=0; tunnels lead to valves XW, LK +Valve GW has flow rate=0; tunnels lead to valves SY, AD +Valve QS has flow rate=0; tunnels lead to valves CC, XW +Valve DF has flow rate=0; tunnels lead to valves AD, CU +Valve XM has flow rate=0; tunnels lead to valves LI, FX +Valve VA has flow rate=0; tunnels lead to valves CU, GH +Valve GA has flow rate=0; tunnels lead to valves IG, AA +Valve YA has flow rate=0; tunnels lead to valves LI, IG +Valve XW has flow rate=20; tunnels lead to valves OB, HQ, QS, WH, AZ +Valve XJ has flow rate=24; tunnel leads to valve LU +Valve AJ has flow rate=0; tunnels lead to valves WG, OP +Valve WH has flow rate=0; tunnels lead to valves XW, NM +Valve TL has flow rate=13; tunnels lead to valves PD, DR, ZU, ND +Valve OD has flow rate=0; tunnels lead to valves CC, VO +Valve ZD has flow rate=0; tunnels lead to valves SS, BP +Valve DR has flow rate=0; tunnels lead to valves DG, TL +Valve WG has flow rate=0; tunnels lead to valves AJ, AA diff --git a/advent/days/day16/solution.py b/advent/days/day16/solution.py new file mode 100644 index 0000000..e1ac546 --- /dev/null +++ b/advent/days/day16/solution.py @@ -0,0 +1,299 @@ +from __future__ import annotations +from abc import ABC, abstractmethod, abstractproperty +from dataclasses import dataclass, field +from itertools import product +from queue import PriorityQueue + +from typing import Iterator, Literal, Self +from advent.parser.parser import P + +day_num = 16 + + +def part1(lines: Iterator[str]) -> int: + system = Network.parse(lines) + return system.under_pressure(30, 1) + + +def part2(lines: Iterator[str]) -> int: + system = Network.parse(lines) + return system.under_pressure(26, 2) + + +valve_parser = P.map3( + P.second(P.string("Valve "), P.upper().word()), + P.second(P.string(" has flow rate="), P.unsigned()), + P.second(P.either(P.string("; tunnels lead to valves "), P.string("; tunnel leads to valve ")), + P.upper().word().sep_by(P.string(", "))), + lambda name, flow_rate, following: RawValve(name, flow_rate, following) +) + + +@dataclass(slots=True) +class RawValve: + name: str + flow_rate: int + following: list[str] + + @classmethod + def parse(cls, line: str) -> Self: + result = valve_parser.parse(line).get() + return result + + +@dataclass(slots=True, unsafe_hash=True) +class Valve: + name: str + flow_rate: int + following: list[Valve] = field(hash=False, compare=False) + paths: dict[str, int] | None = field(default=None, hash=False, init=False, compare=False) + + def __repr__(self) -> str: + return f"{self.name}:{self.flow_rate}->{','.join(v.name for v in self.following)}" + + def travel_time(self, to: str) -> int: + if self.paths is None: + self.create_paths() + return self.paths[to] # type: ignore + + def create_paths(self): + paths: dict[str, int] = {} + to_check: list[tuple[Valve, int]] = [(self, 0)] + while to_check: + current, steps = to_check[0] + to_check = to_check[1:] + + paths[current.name] = steps + for next in current.following: + if paths.get(next.name, steps + 2) > steps + 1: + to_check.append((next, steps + 1)) + self.paths = paths + + +@dataclass(slots=True, frozen=True) +class Actor: + position: Valve + next_time: int + finished: bool + + +@dataclass(slots=True, frozen=True) +class SystemProgress(ABC): + max_time: int + prev_time: int + next_time: int + pressure: int + flow_rate: int + closed_valves: frozenset[Valve] + path: str + + def one_actor(self, actor: Actor) -> Iterator[Actor]: + if actor.finished or actor.next_time != self.next_time: + yield actor + elif not self.closed_valves: + yield Actor(actor.position, self.max_time, True) + else: + reached_any_target = False + for target in self.closed_valves: + finished = self.next_time + actor.position.travel_time(target.name) + 1 + if finished < self.max_time: + reached_any_target = True + yield Actor(target, finished, False) + if not reached_any_target: + yield Actor(actor.position, self.max_time, True) + + @classmethod + def create(cls, max_time: int, pressure: int, flow_rate: int, + closed_valves: frozenset[Valve], start: Valve, + num_actors: Literal[1] | Literal[2]) -> SystemProgress: + match num_actors: + case 1: + return OneActorProgress(max_time, 0, 0, pressure, flow_rate, closed_valves, + start.name, Actor(start, 0, False)) + case 2: + return TwoActorProgress(max_time, 0, 0, pressure, flow_rate, closed_valves, + start.name, Actor(start, 0, False), Actor(start, 0, False)) + case _: + assert False, "Unreachable" + + def __lt__(self, other: OneActorProgress) -> bool: + mx = min(self.next_time, other.next_time) + return self.pressure_at_time(mx) > other.pressure_at_time(mx) + + @abstractmethod + def pressure_at_time(self, time: int) -> int: + ... + + @abstractproperty + def max_possible_pressure(self) -> int: + ... + + @abstractmethod + def open_valves(self) -> Iterator[SystemProgress]: + ... + + +@dataclass(slots=True, frozen=True) +class OneActorProgress(SystemProgress): + actor: Actor + + def pressure_at_time(self, time: int) -> int: + pressure = self.pressure + self.flow_rate * (time - self.prev_time) + if time > self.next_time: + pressure += self.actor.position.flow_rate * (time - self.actor.next_time + 1) + return pressure + + @property + def max_possible_pressure(self) -> int: + closed = sum(valve.flow_rate for valve in self.closed_valves) + return self.pressure + (self.flow_rate + self.actor.position.flow_rate + + closed) * (self.max_time - self.next_time) + + def open_valves(self) -> Iterator[SystemProgress]: + flow_rate = self.flow_rate + self.actor.position.flow_rate + pressure = self.pressure + self.flow_rate * (self.next_time - self.prev_time) + for actor in self.one_actor(self.actor): + closed_valves = self.closed_valves + if not actor.finished: + closed_valves = closed_valves.difference({actor.position}) + path = f"{self.path} {pressure=}\n" \ + f"1: {actor.next_time}->{actor.position.name}\n" \ + f" +{actor.position.flow_rate}\n" + yield OneActorProgress( + max_time=self.max_time, + prev_time=self.next_time, + next_time=actor.next_time, + flow_rate=flow_rate, + pressure=pressure, + closed_valves=closed_valves, + actor=actor, + path=path + ) + + +@dataclass(slots=True, frozen=True) +class TwoActorProgress(SystemProgress): + actor1: Actor + actor2: Actor + + def pressure_at_time(self, time: int) -> int: + pressure = self.pressure + self.flow_rate * (time - self.prev_time) + if time > self.next_time: + if self.actor1.next_time <= time: + pressure += self.actor1.position.flow_rate * (time - self.actor1.next_time + 1) + if self.actor2.next_time <= time: + pressure += self.actor2.position.flow_rate * (time - self.actor2.next_time + 1) + return pressure + + @property + def max_possible_pressure(self) -> int: + closed = sum(valve.flow_rate for valve in self.closed_valves) * \ + (self.max_time - self.next_time - 1) + flow = self.flow_rate * (self.max_time - self.next_time - 1) + if not self.actor1.finished: + actor1 = self.actor1.position.flow_rate * (self.max_time - self.actor1.next_time) + else: + actor1 = 0 + if not self.actor2.finished: + actor2 = self.actor2.position.flow_rate * (self.max_time - self.actor2.next_time) + else: + actor2 = 0 + return self.pressure + actor1 + actor2 + closed + flow + + def open_valves(self) -> Iterator[SystemProgress]: + pressure = self.pressure + self.flow_rate * (self.next_time - self.prev_time) + + flow_rate = self.flow_rate + if self.actor1.next_time == self.next_time: + flow_rate += self.actor1.position.flow_rate + if self.actor2.next_time == self.next_time: + flow_rate += self.actor2.position.flow_rate + + actor1_actions = list(self.one_actor(self.actor1)) + actor2_actions = list(self.one_actor(self.actor2)) + + for actor1, actor2 in product(actor1_actions, actor2_actions): + if not actor1.finished and not actor2.finished and actor1.position == actor2.position: + continue + + closed_valves = self.closed_valves + if not actor1.finished: + closed_valves = closed_valves.difference({actor1.position}) + if not actor2.finished: + closed_valves = closed_valves.difference({actor2.position}) + + next_time = min(actor1.next_time, actor2.next_time) + + path = self.path + next_flow = 0 + if actor1.next_time == next_time: + path += f" -> (S:{actor1.next_time}/{actor1.position.name})" + next_flow += actor1.position.flow_rate + if actor2.next_time == next_time: + path += f" -> (E:{actor2.next_time}/{actor2.position.name})" + next_flow += actor2.position.flow_rate + next_flow = 0 + + yield TwoActorProgress( + max_time=self.max_time, + prev_time=self.next_time, + next_time=next_time, + flow_rate=flow_rate, + pressure=pressure, + closed_valves=closed_valves, + path=path, + actor1=actor1, + actor2=actor2, + ) + + +@dataclass(slots=True) +class Network: + valves: dict[str, Valve] + paths: dict[tuple[str, str], list[str]] = field(default_factory=dict) + + @classmethod + def parse(cls, lines: Iterator[str]) -> Self: + raw_system = [RawValve.parse(line) for line in lines] + valves = {valve.name: Valve(valve.name, valve.flow_rate, []) for valve in raw_system} + for raw in raw_system: + current = valves[raw.name] + for follow in raw.following: + current.following.append(valves[follow]) + + network = Network(valves) + # network.create_paths() + return network + + def under_pressure(self, minutes: int, number_actors: Literal[1] | Literal[2]) -> int: + closed_valves = [valve for valve in self.valves.values() if valve.flow_rate > 0] + start = self.valves["AA"] + queue: PriorityQueue[SystemProgress] = PriorityQueue() + queue.put(SystemProgress.create( + max_time=minutes, + pressure=0, + flow_rate=0, + closed_valves=frozenset(closed_valves), + start=start, + num_actors=number_actors + )) + max_system: SystemProgress | None = None + tick = 0 + max_counter = 0 + while not queue.empty(): + tick += 1 + current = queue.get() + if current.next_time == minutes: + if max_system is None or max_system.pressure < current.pressure: + max_system = current + max_counter += 1 + continue + + for next in current.open_valves(): + if max_system is None or next.max_possible_pressure >= max_system.pressure: + queue.put(next) + + if max_system is None: + raise Exception("No best System found") + + return max_system.pressure diff --git a/advent/days/day16/test_solution.py b/advent/days/day16/test_solution.py new file mode 100644 index 0000000..0390eb0 --- /dev/null +++ b/advent/days/day16/test_solution.py @@ -0,0 +1,38 @@ +from advent.common import input + +from .solution import Network, RawValve, day_num, part1, part2 + + +def test_part1(): + lines = input.read_lines(day_num, 'example01.txt') + expected = 1651 + result = part1(lines) + assert result == expected + + +def test_part2(): + lines = input.read_lines(day_num, 'example01.txt') + expected = 1707 + result = part2(lines) + assert result == expected + + +def test_parse(): + line = "Valve AA has flow rate=0; tunnels lead to valves DD, II, BB" + expected = RawValve("AA", 0, ["DD", "II", "BB"]) + result = RawValve.parse(line) + assert result == expected + + +def test_open_system(): + lines = input.read_lines(day_num, 'example01.txt') + system = Network.parse(lines) + expected = 1651 + assert system.under_pressure(30, 1) == expected + + +def test_open_system_elephant(): + lines = input.read_lines(day_num, 'example01.txt') + system = Network.parse(lines) + expected = 1707 + assert system.under_pressure(26, 2) == expected