Every year, there’s a puzzle past day 20 with an easy part 1 and brutal part 2. I always feel a vague sense of dread while solving part 1, which makes them psychologically interesting.
This year, it was day 23. The puzzle asks you to find the longest path through a maze that does not visit any tile twice. Some tiles are marked with a steep slope that limits travel (e.g., >
means you can only go right on that tile). Brute-forcing was pretty easy.
Part 2, of course, changes that. Now you can go in any direction on sloped tiles. The astute among you have already noted that the longest path in a graph with cycles is an NP-hard problem. That meant brute force was hopeless.
I wasted a lot of time with alternative approaches before I found a guide that recommended reducing the maze to a graph of intersections separated by distances. Once given the approach, implementing it was easy.
```{python}
def node_paths(start, neighbors, targets):
= deque([[start]])
queue
while queue:
= queue.pop()
current = current[-1]
current_coord
for neighbor in neighbors(current_coord):
if neighbor not in current:
# New path found to node, so end here
if neighbor in targets and neighbor != current_coord:
# Longer path found
yield start, neighbor, len(current)
else:
+ [neighbor])
queue.append(current
def reduce_graph(graph, start, goal, neighbors):
= {c for c in graph.keys() if len(neighbors(c)) > 2} | {start, goal}
targets = defaultdict(dict)
result for target in targets:
= node_paths(target, neighbors, targets)
gen # Exhaust results from generator
while True:
try:
= next(gen)
this if this is not None:
= this
start, dest, length = max(result[start].get(dest, -inf), length)
result[start][dest] = max(result[dest].get(start, -inf), length)
result[dest][start] except StopIteration:
break
return result
def solve(graph, start, goal):
= deque([(set(), start, 0)])
queue = -inf
result
while queue:
= queue.pop()
current, current_coord, current_dist if current_coord == goal:
= max(result, current_dist)
result else:
= graph[current_coord]
neighbors for neighbor, dist in neighbors.items():
if neighbor not in current:
= current_dist + dist
new_dist | {current_coord}, neighbor, new_dist))
queue.append((current return result
```