Solve 2016:11 p1-2 "Radioisotope Thermoelectric Generators"

Hard one, an infamous AoC puzzle according to
Reddit.

Apparently, this is a classic logic problem named
"Missionaries and cannibals", or "Jealous husbands".

Hard lessons:

- Always use set() for visited nodes during BFS.
- Always use collections.queue() to create to queue
  traversals for BFS.
- itertools permutations(), combinations() and
pairwise() may look similar, but they are not.
- Learned to use (?:chunk of text)? in regex.

Test data was achieved without bigger hazzles, but
to optimize code required a lot of Reddit browsing
and code reading from blogs. I have mentioned the
sources in a doc string.
This commit is contained in:
Anders Englöf Ytterström 2024-11-28 23:02:29 +01:00
parent b1b5cda1e3
commit c113042e17

View file

@ -0,0 +1,165 @@
import re
from collections import Counter, deque
from itertools import combinations
from output import answer # , matrix, D, DD, ADJ, ints, mhd, mdbg, vdbg
n = 11
title = "Radioisotope Thermoelectric Generators"
D = {1: [2], 2: [1, 3], 3: [2, 4], 4: [3]}
@answer(1, "To transport all objects to the 4th floor, {} steps are required")
def part_1(presolved):
return presolved[0]
@answer(2, "With the additonal objects to transport, {} steps are required")
def part_2(presolved):
return presolved[1]
def solve(data):
def parse(row):
return sorted(
[
"".join(nt).upper()
for nt in re.findall(r"(\w)\S+(?:-compatible)? (g|m)", row)
]
)
f = dict([(i, parse(row)) for i, row in enumerate(data.splitlines(), start=1)])
E = sum(map(len, f.values()))
p1 = bfs(1, f, E)
adjusted = data.splitlines()
adjusted[
0
] += """
An elerium generator.
An elerium-compatible microchip.
A dilithium generator.
A dilithium-compatible microchip.
"""
f = dict([(i, parse(row)) for i, row in enumerate(adjusted, start=1)])
E = sum(map(len, f.values()))
p2 = bfs(1, f, E)
return p1, p2
def bfs(S, m, E):
seen = set()
q = deque([(S, m, 0)])
while q:
e, m, w = q.popleft()
cs = seen_checksum(e, m) # reddit wisdom, see function for more info
if cs in seen:
continue
seen.add(cs)
if len(m[4]) == E:
return w
for n, b, a in valid_next_moves(e, m):
ns = m.copy()
ns[e] = a
ns[n] = b
q.append((n, ns, w + 1))
return None
def valid_next_moves(e, S):
g = []
for n in D[e]:
for o in [[x] for x in S[e]] + [list(x) for x in combinations(S[e], 2)]:
a = sorted(x for x in S[e] if x not in o)
b = sorted(S[n] + o)
if is_valid(a) and is_valid(b):
g.append((n, b, a))
return g
def is_valid(f):
g = [x for x in f if x.endswith("G")]
if not g:
return True
mc = [x for x in f if x.endswith("M")]
return all(f"{m[0]}G" in f for m in mc)
def seen_checksum(e, s):
# To speed up execution, a handy trick was mentioned on several
# reddit threads.
#
# The vanilla BFS method is to store the complete state (elevator
# position + all floors), which this code like many others did initially.
# This is fine, but will lead to really long execution times.
#
# The common wisdom boils down to one thing: it does not matter _what_
# name the microchips and generators have. Only the arrangement (counts) of
# any microchips or generators across the floors do.
#
# For example, these are the same as a checksum to determine if a state has
# been seen:
#
# F2 . HG . . F2 . LG . .
# F1 E . HM LM F1 E . LM HM
#
# The H's or L's do not matter, only the M's and G's do.
#
# So by storing the M's and the G's as a checksum, along with the
# elevator position, the program is a lot faster.
#
# Reddit post, giving hints:
# https://www.reddit.com/r/adventofcode/comments/5hoia9/comment/db1v1ws/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
# Blog post, providing code and reasoning:
# https://eddmann.com/posts/advent-of-code-2016-day-11-radioisotope-thermoelectric-generators/
# uncomment below line to get speed boost:
# %> 2.59s user 0.02s system 99% cpu 2.622 total
return e, tuple(tuple(Counter(x[-1] for x in f).most_common()) for f in s.values())
# uncomment below line to use vanilla BFS visited storage
# %> 896.11s user 2.58s system 99% cpu 15:01.35 total
# return e, tuple((f, tuple(mg)) for f, mg in s.items())
def M(s, e):
d = {}
for k, v in s.items():
for x in v:
d[x] = k - 1
l = len(d)
d = [
[v if x == k else ". " for x in range(l)]
for v, k in sorted(d.items(), key=lambda kv: kv[0])
]
m = [("F1", "F2", "F3", "F4"), ["E " if x == e - 1 else ". " for x in range(l)]] + d
for r in list(zip(*m))[::-1]:
print(" ".join(r))
print("")
if __name__ == "__main__":
# use dummy data
inp = """
The first floor contains a hydrogen-compatible microchip and a lithium-compatible microchip.
The second floor contains a hydrogen generator.
The third floor contains a lithium generator.
The fourth floor contains nothing relevant.
""".strip()
z, _ = solve(inp)
with open("./input/11.txt", "r") as f:
inp = f.read().strip()
inp = solve(inp)
a = part_1(inp)
b = part_2(inp)
assert z == 11
assert a == 37
assert b == 61