Skip to content

Commit c6daaba

Browse files
committed
feat: aoc 2022 day 16 solution + tests
1 parent d83a050 commit c6daaba

File tree

5 files changed

+371
-0
lines changed

5 files changed

+371
-0
lines changed

_2022/data/day16/puzzle_input.txt

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
Valve FY has flow rate=0; tunnels lead to valves TG, CD
2+
Valve EK has flow rate=12; tunnels lead to valves JE, VE, PJ, CS, IX
3+
Valve NU has flow rate=0; tunnels lead to valves FG, HJ
4+
Valve AY has flow rate=0; tunnels lead to valves EG, KR
5+
Valve DH has flow rate=0; tunnels lead to valves FX, VW
6+
Valve IX has flow rate=0; tunnels lead to valves VW, EK
7+
Valve DZ has flow rate=0; tunnels lead to valves HT, FG
8+
Valve YE has flow rate=0; tunnels lead to valves CI, MS
9+
Valve OO has flow rate=0; tunnels lead to valves FX, CS
10+
Valve SB has flow rate=0; tunnels lead to valves RR, AP
11+
Valve HT has flow rate=4; tunnels lead to valves DZ, GA, CI, DE, JS
12+
Valve MS has flow rate=11; tunnels lead to valves PJ, WG, CA, YE
13+
Valve CD has flow rate=0; tunnels lead to valves UW, FY
14+
Valve IZ has flow rate=0; tunnels lead to valves XF, AP
15+
Valve JE has flow rate=0; tunnels lead to valves EK, TQ
16+
Valve DN has flow rate=0; tunnels lead to valves KR, VE
17+
Valve VW has flow rate=13; tunnels lead to valves DH, IX
18+
Valve UH has flow rate=0; tunnels lead to valves MN, TQ
19+
Valve TB has flow rate=0; tunnels lead to valves AP, BJ
20+
Valve XT has flow rate=0; tunnels lead to valves TQ, UW
21+
Valve RR has flow rate=0; tunnels lead to valves FG, SB
22+
Valve BJ has flow rate=0; tunnels lead to valves TB, AA
23+
Valve DE has flow rate=0; tunnels lead to valves HT, WI
24+
Valve MT has flow rate=0; tunnels lead to valves EW, FG
25+
Valve HJ has flow rate=0; tunnels lead to valves KS, NU
26+
Valve WI has flow rate=3; tunnels lead to valves XF, DX, DE, EW
27+
Valve KI has flow rate=0; tunnels lead to valves GW, TQ
28+
Valve JS has flow rate=0; tunnels lead to valves UW, HT
29+
Valve XF has flow rate=0; tunnels lead to valves WI, IZ
30+
Valve VE has flow rate=0; tunnels lead to valves DN, EK
31+
Valve CI has flow rate=0; tunnels lead to valves YE, HT
32+
Valve GW has flow rate=0; tunnels lead to valves EG, KI
33+
Valve TQ has flow rate=14; tunnels lead to valves WG, KI, JE, UH, XT
34+
Valve AA has flow rate=0; tunnels lead to valves BJ, CF, DX, RB, AQ
35+
Valve EW has flow rate=0; tunnels lead to valves MT, WI
36+
Valve UW has flow rate=6; tunnels lead to valves XT, CD, NZ, JS
37+
Valve MN has flow rate=0; tunnels lead to valves KR, UH
38+
Valve FG has flow rate=8; tunnels lead to valves NU, RR, MT, MK, DZ
39+
Valve RB has flow rate=0; tunnels lead to valves NZ, AA
40+
Valve AQ has flow rate=0; tunnels lead to valves AA, MK
41+
Valve WG has flow rate=0; tunnels lead to valves TQ, MS
42+
Valve YW has flow rate=0; tunnels lead to valves CA, KR
43+
Valve CA has flow rate=0; tunnels lead to valves YW, MS
44+
Valve PJ has flow rate=0; tunnels lead to valves MS, EK
45+
Valve EG has flow rate=23; tunnels lead to valves AY, GW
46+
Valve NC has flow rate=0; tunnels lead to valves TG, KS
47+
Valve WY has flow rate=16; tunnel leads to valve VQ
48+
Valve AP has flow rate=7; tunnels lead to valves IZ, VQ, TB, SB
49+
Valve CF has flow rate=0; tunnels lead to valves GA, AA
50+
Valve FX has flow rate=20; tunnels lead to valves DH, OO
51+
Valve NZ has flow rate=0; tunnels lead to valves RB, UW
52+
Valve KS has flow rate=19; tunnels lead to valves NC, HJ
53+
Valve VQ has flow rate=0; tunnels lead to valves WY, AP
54+
Valve TG has flow rate=17; tunnels lead to valves NC, FY
55+
Valve GA has flow rate=0; tunnels lead to valves CF, HT
56+
Valve CS has flow rate=0; tunnels lead to valves OO, EK
57+
Valve MK has flow rate=0; tunnels lead to valves AQ, FG
58+
Valve KR has flow rate=18; tunnels lead to valves MN, DN, YW, AY
59+
Valve DX has flow rate=0; tunnels lead to valves AA, WI

_2022/solutions/day16.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
"""Day 16: Proboscidea Volcanium
2+
3+
This module provides the solution for Advent of Code 2022 - Day 16.
4+
5+
It models valve opening optimization in a volcanic tunnel network, using
6+
dynamic programming to maximize pressure release within time constraints.
7+
8+
The module contains a cached helper function and a Solution class that
9+
inherits from SolutionBase for parsing valve networks and computing optimal
10+
valve opening sequences.
11+
"""
12+
13+
from functools import cache
14+
import re
15+
from typing import ClassVar
16+
17+
import rustworkx as rx
18+
19+
from aoc.models.base import SolutionBase
20+
21+
22+
@cache
23+
def _max_pressure_helper(
24+
current_valve: str,
25+
time_left: int,
26+
unopened: frozenset[str],
27+
flow_rates: tuple[tuple[str, int], ...],
28+
distances: tuple[tuple[tuple[str, str], int], ...],
29+
) -> int:
30+
"""Calculate maximum pressure releasable from current state using memoization.
31+
32+
Recursively explores all possible valve opening sequences, caching results
33+
to avoid recomputation of identical states. Uses immutable types for caching.
34+
35+
Args:
36+
current_valve: Current position in valve network
37+
time_left: Remaining minutes before eruption
38+
unopened: Frozenset of valves not yet opened
39+
flow_rates: Tuple of (valve, rate) pairs for caching
40+
distances: Tuple of ((valve1, valve2), distance) pairs for caching
41+
42+
Returns
43+
-------
44+
int: Maximum pressure that can be released from this state
45+
"""
46+
if time_left <= 0 or not unopened:
47+
return 0
48+
49+
flow_rates_dict = dict(flow_rates)
50+
distances_dict = dict(distances)
51+
52+
best = 0
53+
54+
for valve in unopened:
55+
travel_time = distances_dict[(current_valve, valve)]
56+
time_after_opening = time_left - travel_time - 1
57+
58+
if time_after_opening > 0:
59+
pressure = flow_rates_dict[valve] * time_after_opening
60+
remaining = unopened - {valve}
61+
62+
total = pressure + _max_pressure_helper(
63+
valve, time_after_opening, remaining, flow_rates, distances
64+
)
65+
best = max(best, total)
66+
67+
return int(best)
68+
69+
70+
class Solution(SolutionBase):
71+
"""Optimize valve opening sequence to maximize pressure release in volcano.
72+
73+
This solution models a tunnel network with pressure-release valves. Part 1
74+
finds the optimal sequence to open valves alone in 30 minutes. Part 2 solves
75+
for two agents (you and an elephant) working in parallel for 26 minutes.
76+
77+
Uses graph algorithms to precompute shortest paths between important valves,
78+
then dynamic programming with memoization to explore valve opening sequences.
79+
"""
80+
81+
VALVE_REGEX: ClassVar[re.Pattern[str]] = re.compile(r"([A-Z]{2})")
82+
FLOW_RATE_REGEX: ClassVar[re.Pattern[str]] = re.compile(r"rate=(\d+)")
83+
84+
def parse_data(
85+
self, data: list[str]
86+
) -> tuple[tuple[tuple[str, int], ...], tuple[tuple[tuple[str, str], int], ...]]:
87+
"""Parse valve network into flow rates and distances between valves.
88+
89+
Constructs a graph of tunnel connections and uses Dijkstra's algorithm
90+
to compute shortest paths between all important valves (AA start position
91+
and any valve with positive flow rate).
92+
93+
Args:
94+
data: List of strings describing valves and tunnel connections
95+
96+
Returns
97+
-------
98+
tuple: Immutable flow_rates mapping and distances mapping for caching
99+
"""
100+
flow_rates: dict[str, int] = {}
101+
distances: dict[tuple[str, str], int] = {}
102+
103+
graph: rx.PyGraph = rx.PyGraph()
104+
valve_to_idx: dict[str, int] = {}
105+
idx_to_valve: dict[int, str] = {}
106+
107+
for line in data:
108+
valve_match = re.search(self.VALVE_REGEX, line)
109+
rate_match = re.search(self.FLOW_RATE_REGEX, line)
110+
111+
if valve_match is None or rate_match is None:
112+
err_msg = f"Invalid line format: {line}"
113+
raise ValueError(err_msg)
114+
115+
valve = valve_match.group(1)
116+
rate = int(rate_match.group(1))
117+
118+
idx = graph.add_node(valve)
119+
valve_to_idx[valve] = idx
120+
idx_to_valve[idx] = valve
121+
122+
if rate > 0:
123+
flow_rates[valve] = rate
124+
125+
for line in data:
126+
valve_match = re.search(self.VALVE_REGEX, line)
127+
if valve_match is None:
128+
err_msg = f"Invalid line format: {line}"
129+
raise ValueError(err_msg)
130+
131+
valve = valve_match.group(1)
132+
tunnels = re.findall(r"[A-Z]{2}", line.split("valve")[-1])
133+
valve_idx = valve_to_idx[valve]
134+
135+
for neighbor in tunnels:
136+
graph.add_edge(valve_idx, valve_to_idx[neighbor], 1)
137+
138+
important = ["AA", *list(flow_rates.keys())]
139+
140+
for idx, v1 in enumerate(important):
141+
for v2 in important[idx + 1 :]:
142+
idx1, idx2 = valve_to_idx[v1], valve_to_idx[v2]
143+
144+
dist = rx.dijkstra_shortest_path_lengths(
145+
graph, idx1, edge_cost_fn=lambda _: 1, goal=idx2
146+
)[idx2]
147+
148+
distances[(v1, v2)] = distances[(v2, v1)] = dist
149+
150+
return tuple(flow_rates.items()), tuple(distances.items())
151+
152+
def dfs(
153+
self,
154+
current: str,
155+
time: int,
156+
unopened: frozenset[str],
157+
opened: frozenset[str],
158+
pressure: int,
159+
flow_rates: dict[str, int],
160+
distances: dict[tuple[str, str], int],
161+
results: dict[frozenset[str], int],
162+
) -> None:
163+
"""Depth-first search to find all reachable valve combinations and pressures.
164+
165+
Explores all possible paths through the valve network, recording the best
166+
pressure achievable for each unique set of opened valves. Used in Part 2
167+
to find complementary valve sets for parallel agents.
168+
169+
Args:
170+
current: Current valve position
171+
time: Remaining time
172+
unopened: Set of valves not yet opened
173+
opened: Set of valves already opened
174+
pressure: Total pressure released so far
175+
flow_rates: Valve flow rate mappings
176+
distances: Shortest path distances between valves
177+
results: Dictionary to accumulate (opened_set -> best_pressure) mappings
178+
"""
179+
if opened not in results or pressure > results[opened]:
180+
results[opened] = pressure
181+
182+
for valve in unopened:
183+
travel_time = distances[(current, valve)]
184+
time_after = time - travel_time - 1
185+
186+
if time_after > 0:
187+
valve_pressure = flow_rates[valve] * time_after
188+
self.dfs(
189+
valve,
190+
time_after,
191+
unopened - {valve},
192+
opened | {valve},
193+
pressure + valve_pressure,
194+
flow_rates,
195+
distances,
196+
results,
197+
)
198+
199+
def part1(self, data: list[str]) -> int:
200+
"""Find maximum pressure releasable by opening valves alone in 30 minutes.
201+
202+
Starting at valve AA with 30 minutes, determines the optimal sequence
203+
of valve openings to maximize total pressure released before volcanic
204+
eruption. Travel between valves and opening each valve costs 1 minute.
205+
206+
Args:
207+
data: List of strings describing valve network
208+
209+
Returns
210+
-------
211+
int: Maximum total pressure that can be released
212+
"""
213+
flow_rates, distances = self.parse_data(data)
214+
215+
return _max_pressure_helper(
216+
"AA", 30, frozenset(dict(flow_rates).keys()), flow_rates, distances
217+
)
218+
219+
def part2(self, data: list[str]) -> int:
220+
"""Find maximum pressure with you and elephant working in parallel for 26 minutes.
221+
222+
After spending 4 minutes teaching the elephant, you both have 26 minutes
223+
to open valves. Finds the optimal division of valves between two agents
224+
by exploring all possible combinations and selecting non-overlapping sets
225+
with maximum combined pressure.
226+
227+
Args:
228+
data: List of strings describing valve network
229+
230+
Returns
231+
-------
232+
int: Maximum combined pressure achievable with two parallel agents
233+
"""
234+
flow_rates, distances = self.parse_data(data)
235+
all_valves = frozenset(dict(flow_rates).keys())
236+
237+
results: dict[frozenset[str], int] = {frozenset(): 0}
238+
239+
self.dfs("AA", 26, all_valves, frozenset(), 0, dict(flow_rates), dict(distances), results)
240+
241+
max_pressure = 0
242+
items = list(results.items())
243+
244+
for i in range(len(items)):
245+
for j in range(i, len(items)):
246+
combo1, pressure1 = items[i]
247+
combo2, pressure2 = items[j]
248+
249+
if not combo1 & combo2:
250+
max_pressure = max(max_pressure, pressure1 + pressure2)
251+
252+
return int(max_pressure)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Valve AA has flow rate=0; tunnels lead to valves DD, II, BB
2+
Valve BB has flow rate=13; tunnels lead to valves CC, AA
3+
Valve CC has flow rate=2; tunnels lead to valves DD, BB
4+
Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE
5+
Valve EE has flow rate=3; tunnels lead to valves FF, DD
6+
Valve FF has flow rate=0; tunnels lead to valves EE, GG
7+
Valve GG has flow rate=0; tunnels lead to valves FF, HH
8+
Valve HH has flow rate=22; tunnel leads to valve GG
9+
Valve II has flow rate=0; tunnels lead to valves AA, JJ
10+
Valve JJ has flow rate=21; tunnel leads to valve II
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Valve AA has flow rate=0; tunnels lead to valves DD, II, BB
2+
Valve BB has flow rate=13; tunnels lead to valves CC, AA
3+
Valve CC has flow rate=2; tunnels lead to valves DD, BB
4+
Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE
5+
Valve EE has flow rate=3; tunnels lead to valves FF, DD
6+
Valve FF has flow rate=0; tunnels lead to valves EE, GG
7+
Valve GG has flow rate=0; tunnels lead to valves FF, HH
8+
Valve HH has flow rate=22; tunnel leads to valve GG
9+
Valve II has flow rate=0; tunnels lead to valves AA, JJ
10+
Valve JJ has flow rate=21; tunnel leads to valve II

_2022/tests/test_16.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Test suite for Day 16: Proboscidea Volcanium
2+
3+
This module contains tests for the Day 16 solution, which optimizes valve
4+
opening sequences in a volcanic tunnel network. The tests verify:
5+
6+
1. Part 1: Maximum pressure with single agent in 30 minutes
7+
2. Part 2: Maximum pressure with two parallel agents in 26 minutes
8+
"""
9+
10+
from aoc.models.tester import TestSolutionUtility
11+
12+
13+
def test_day16_part1() -> None:
14+
"""Test maximizing pressure release with single agent.
15+
16+
This test runs the solution for Part 1 of the puzzle against the
17+
provided test input and compares the result with the expected output.
18+
"""
19+
TestSolutionUtility.run_test(
20+
year=2022,
21+
day=16,
22+
is_raw=False,
23+
part_num=1,
24+
expected=1651,
25+
)
26+
27+
28+
def test_day16_part2() -> None:
29+
"""Test maximizing pressure release with two parallel agents.
30+
31+
This test runs the solution for Part 2 of the puzzle against the
32+
provided test input and compares the result with the expected output.
33+
"""
34+
TestSolutionUtility.run_test(
35+
year=2022,
36+
day=16,
37+
is_raw=False,
38+
part_num=2,
39+
expected=1707,
40+
)

0 commit comments

Comments
 (0)