2024 day 21 part 2.
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Mikaël Capelle 2024-12-24 14:27:27 +01:00
parent d61d9d4559
commit 1e2db73b52

View File

@ -1,121 +1,85 @@
import heapq import itertools
from dataclasses import dataclass from functools import cache
from typing import Any, Iterator, Literal, Sequence, TypeAlias, cast from typing import Any, Iterator, Literal
from ..base import BaseSolver from ..base import BaseSolver
Action: TypeAlias = Literal[">", "<", "v", "^", "A"] NUM_PAD_P = {
v: (i, j)
NUM_PAD = ((7, 8, 9), (4, 5, 6), (1, 2, 3), (None, 0, "A")) for i, r in enumerate(("789", "456", "123", " 0A"))
MOV_PAD: tuple[tuple[Action | None, ...], ...] = ((None, "^", "A"), ("<", "v", ">")) for j, v in enumerate(r)
if v.strip()
}
MOV_PAD_P = {
v: (i, j)
for i, r in enumerate((" ^A", "<v>"))
for j, v in enumerate(r)
if v.strip()
}
@dataclass(frozen=True, order=True) def path(start: tuple[int, int], end: tuple[int, int], pad: Literal["num", "mov"]):
class Node: # a move in the grid is composed of at most two straight line: up/down and
robot_1: tuple[int, int] = (0, 2) # left/right, since doing some kind of diagonal moves would create long path for
robot_2: tuple[int, int] = (0, 2) # the robot above (since this involves going back-and-forth to the letter 'A')
robot_3: tuple[int, int] = (3, 2) #
row_s, col_s = start
row_e, col_e = end
code: str = "" le, de, ue, re = (
"<" * max(0, col_s - col_e),
"v" * max(0, row_e - row_s),
def apply_action( "^" * max(0, row_s - row_e),
robot: tuple[int, int], ">" * max(0, col_e - col_s),
action: Action,
pad: tuple[tuple[int | str | None, ...], ...],
):
d_row, d_col = {"^": (-1, 0), "v": (1, 0), ">": (0, 1), "<": (0, -1)}[action]
row, col = robot[0] + d_row, robot[1] + d_col
if 0 <= row < len(pad) and 0 <= col < len(pad[row]) and pad[row][col] is not None:
return (row, col)
return None
def create_node(node: Node, action: Action) -> Node | None:
# main pad moves -> move first robot
if action != "A":
robot = apply_action(node.robot_1, action, MOV_PAD)
if robot is not None:
return Node(
robot_1=robot,
robot_2=node.robot_2,
robot_3=node.robot_3,
code=node.code,
) )
return None # when the robot starts or ends on the row/column with the empty cell, there is
# only one way to move
#
if pad == "num" and (row_s, col_e) == (3, 0):
return ue + le
elif pad == "num" and (col_s, row_e) == (0, 3):
return re + de
elif pad == "mov" and col_s == 0:
return re + ue
elif pad == "mov" and col_e == 0:
return de + le
# activate pad 1 -> action on robot 1 # otherwise, we need to decide if we want to go up/down first, or left/right, and
robot_1_action = MOV_PAD[node.robot_1[0]][node.robot_1[1]] # apparently this is the best way to do it...
assert robot_1_action is not None return le + de + ue + re
if robot_1_action != "A":
robot2 = apply_action(node.robot_2, robot_1_action, MOV_PAD) @cache
if robot2 is not None: def v_clicks(clicks: str, depth: int) -> int:
return Node( if depth == 0:
robot_1=node.robot_1, return len(clicks)
robot_2=robot2,
robot_3=node.robot_3, n_clicks = 0
code=node.code, at = "A"
for _, group in itertools.groupby(clicks):
group = list(group)
n_clicks += v_clicks(
path(MOV_PAD_P[at], MOV_PAD_P[group[0]], "mov") + "A" * len(group),
depth - 1,
) )
return None at = group[0]
# activate pad 2 -> action on robot 2 return n_clicks
robot_2_action = MOV_PAD[node.robot_2[0]][node.robot_2[1]]
assert robot_2_action is not None
if robot_2_action != "A":
robot3 = apply_action(node.robot_3, robot_2_action, NUM_PAD)
if robot3 is not None:
return Node(
robot_1=node.robot_1,
robot_2=node.robot_2,
robot_3=robot3,
code=node.code,
)
return None
value = NUM_PAD[node.robot_3[0]][node.robot_3[1]] def path_length(code: str, depth: int):
assert value is not None return sum(
return Node( v_clicks(path(NUM_PAD_P[start], NUM_PAD_P[end], "num") + "A", depth)
robot_1=node.robot_1, for start, end in zip("A" + code[:-1], code, strict=True)
robot_2=node.robot_2,
robot_3=node.robot_3,
code=node.code + str(value),
) )
class Solver(BaseSolver): class Solver(BaseSolver):
def dijkstra_for_code(self, target: str):
queue: list[tuple[float, Node, tuple[str, ...]]] = [(0, Node(), ())]
preds: dict[Node, tuple[str, ...]] = {}
while queue:
dis, node, path = heapq.heappop(queue)
if not target.startswith(node.code):
continue
if node in preds:
continue
preds[node] = path
if node.code == target:
self.logger.info(f"found [{target}]: {''.join(path)} ({len(path)})")
return path
for action in cast(Sequence[Action], "A^v<>"):
node_2 = create_node(node, action)
if node_2:
heapq.heappush(queue, (dis + 1, node_2, path + (action,)))
return None
def solve(self, input: str) -> Iterator[Any]: def solve(self, input: str) -> Iterator[Any]:
yield sum( yield sum(
len(self.dijkstra_for_code(code) or ()) * int(code[:-1], 10) path_length(code, 2) * int(code[:-1], 10) for code in input.splitlines()
for code in input.splitlines() )
yield sum(
path_length(code, 25) * int(code[:-1], 10) for code in input.splitlines()
) )