···44from itertools import permutations
55from concurrent.futures import ProcessPoolExecutor
6677-from utils import parse_line, memoize
88-99-from more_itertools import set_partitions
77+from utils import parse_line
1081191210GRAPH = defaultdict(set)
···60586159# Define helper functions to operate on bitstring representations
6260# of which valves have been visited, rather than using sets.
6363-def bitstring_for_subgraph(nodes):
6464- bitstring = 0b0
6565- for n in nodes:
6666- bitstring |= (1 << REAL[n])
6767-6868- return bitstring
6969-7061def is_node_set_in_bitstring(bitstring, node):
7162 return bool(bitstring & (1 << REAL[node]))
7263···7768 return bitstring + 1 == (1 << len(REAL))
786979708080-@memoize
8181-def search(nodes, max_time):
7171+def search(init_opened, max_time):
8272 best = 0
8373 seen = {}
8474 horizon = deque()
85758676 # Recompute compressed graph based on limited nodes available.
8777 compressed = {
8888- a: [(n, d) for n, d in b if n in nodes]
8989- for a, b in COMPRESSED.items() if a in nodes
7878+ a: [(n, d) for n, d in b if not is_node_set_in_bitstring(init_opened, n)]
7979+ for a, b in COMPRESSED.items() if not is_node_set_in_bitstring(init_opened, a)
9080 }
91819282 # Seed start positions with time and pressure based on the time it takes
9383 # to get from AA to that valve, + 1 minute to open the valve.
9494- for node in nodes:
8484+ for node in REAL:
8585+ if is_node_set_in_bitstring(init_opened, node):
8686+ continue
8787+9588 start_time = START_TO_REAL[node] + 1
9689 start_pressure = (max_time - start_time) * FLOW[node]
9797- state = (start_time, node, start_pressure, bitstring_for_subgraph([node]))
9090+ start_opened = set_node_in_bitstring(init_opened, node)
9191+ state = (start_time, node, start_pressure, start_opened)
9892 horizon.append(state)
999310094 while horizon:
···115109 best = pressure
116110117111 # move
118118- for n, dist in compressed[curr]:
112112+ for n, dist in COMPRESSED[curr]:
119113 if not is_node_set_in_bitstring(opened, n):
120114 new_time = time + dist + 1 # time to move + 1 minute to open
121115 extra_pressure = (max_time - new_time) * FLOW[n]
···128122129123130124# Solve part 1.
131131-print("Part 1:", search(REAL, 30))
125125+print("Part 1:", search(0b0, 30))
132126133127# Solve part 2.
134128def dual_search(partition):
···136130 return search(a, 26) + search(b, 26)
137131138132part_2 = 0
139139-partitions = list(set_partitions(REAL, 2))
133133+134134+# Given the bitstring representation of opened nodes, the set of all possibilities is
135135+# just all pairs of numbers that sum to 2^(len(REAL)) - 1. We iterate over the range
136136+# of half this amount to ensure we don't double-up on pairs, thus removing the need
137137+# for any sort of memoization (we compute precisely the results we need).
138138+target = (1 << len(REAL)) - 1
139139+partitions = [(n, target - n) for n in range(1 << (len(REAL) - 1))]
140140141141# This is not the optimal solution yet. If it were, I wouldn't be parallelizing.
142142-parallel = True
142142+parallel = False
143143144144if parallel:
145145 with ProcessPoolExecutor() as pool:
···156156 except ImportError:
157157 loading = lambda x, total: x
158158159159- for p in loading(set_partitions(REAL, 2), total=len(partitions)):
159159+ for p in loading(partitions, total=len(partitions)):
160160 part_2 = max(part_2, dual_search(p))
161161162162print("Part 2:", part_2)