diff --git a/challenges/2023/23-aLongWalk/README.md b/challenges/2023/23-aLongWalk/README.md new file mode 100644 index 0000000..6b64953 --- /dev/null +++ b/challenges/2023/23-aLongWalk/README.md @@ -0,0 +1,6 @@ +# [Day 23: A Long Walk](https://adventofcode.com/2023/day/23) + +Part 2 is *not*: +* 4717 (too low) +* 4826 (too low) +* 5350 (too low) \ No newline at end of file diff --git a/challenges/2023/23-aLongWalk/main.py b/challenges/2023/23-aLongWalk/main.py new file mode 100644 index 0000000..805c258 --- /dev/null +++ b/challenges/2023/23-aLongWalk/main.py @@ -0,0 +1,222 @@ +import sys +import gridutil.grid as gu +import gridutil.coord as cu +from collections import namedtuple +from collections.abc import Generator + + +def parse(instr: str) -> gu.Grid: + return gu.parse(instr) + + +def get_terminal_points(forest: gu.Grid) -> tuple[cu.Coordinate, cu.Coordinate]: + start_pos = None + end_pos = None + max_y = gu.get_max_y(forest) + for x in range(gu.get_max_x(forest) + 1): + if forest[(x, 0)] == "." and start_pos is None: + start_pos = cu.Coordinate(x, 0) + if forest[(x, max_y)] == "." and end_pos is None: + end_pos = cu.Coordinate(x, max_y) + + if start_pos is None: + raise ValueError("No open cell found on the first row of the forest") + + if end_pos is None: + raise ValueError("No open cell found on the last row of the forest") + + return start_pos, end_pos + + +def trace_path( + forest: gu.Grid, + start_pos: cu.Coordinate, + end_pos: cu.Coordinate, + history: set[cu.Coordinate] | None = None, + n: int = 0, + disallowed_moves: dict[str, cu.Direction] | None = None, +) -> int: + if history is None: + history = set() + + if disallowed_moves is None: + disallowed_moves = {} + + cursor = start_pos + while cursor != end_pos: + history.add(cursor) + allowable = [] + for direction in cu.Direction: + target_pos = cu.add(cursor, direction.delta()) + + if target_pos not in forest or target_pos in history: + continue + + val = forest[target_pos] + + if val == "#" or direction == disallowed_moves.get(val): + continue + + allowable.append(target_pos) + + n += 1 + + match len(allowable): + case 0: + return -1 + case 1: + cursor = allowable[0] + case _: + highest = max( + trace_path( + forest, + x, + end_pos, + history=history.copy(), + n=n, + disallowed_moves=disallowed_moves, + ) + for x in allowable + ) + return highest + + return n + + +def one(instr: str): + forest = parse(instr) + start_pos, end_pos = get_terminal_points(forest) + return trace_path( + forest, + start_pos, + end_pos, + disallowed_moves={ + "^": cu.Direction.Down, + "v": cu.Direction.Up, + ">": cu.Direction.Left, + "<": cu.Direction.Right, + }, + ) + + +DistanceTo: tuple[cu.Coordinate, int] = namedtuple( + "DistanceTo", ["coordinate", "distance"] +) + + +def neighbours( + forest: gu.Grid, pos: cu.Coordinate +) -> Generator[cu.Coordinate, None, None]: + for direction in cu.Direction: + target = cu.add(pos, direction.delta()) + if target not in forest or forest[target] == "#": + continue + yield target + + +def build_graph( + forest: gu.Grid, start_pos: cu.Coordinate +) -> dict[cu.Coordinate, list[DistanceTo]]: + junctions = [start_pos] + graph = {} + + while junctions: + st = junctions.pop(0) + + possible_next = list(neighbours(forest, st)) + routes = [] + + for start_point in possible_next: + n = 0 + cursor = start_point + history = set([start_point, st]) + get_next_steps = lambda: list( + filter( + lambda x: x not in history, + neighbours(forest, cursor), + ) + ) + next_steps = get_next_steps() + + while len(next_steps) == 1: + n += 1 + cursor = next_steps[0] + history.add(cursor) + next_steps = get_next_steps() + + routes.append(DistanceTo(cursor, n + 1)) + if cursor not in graph: + junctions.append(cursor) + + graph[st] = routes + + return graph + + +def display_graph(graph: dict[cu.Coordinate, list[DistanceTo]]): + import networkx as nx + import matplotlib.pyplot as plt + + G = nx.Graph() + + for node in graph: + for (end, length) in graph[node]: + G.add_edge(node, end, weight=length) + + nx.draw(G, with_labels=True, font_weight="bold") + plt.show() + + +def trace_graph_path( + graph: dict[cu.Coordinate, list[DistanceTo]], + start_pos: cu.Coordinate, + end_pos: cu.Coordinate, + history: set[cu.Coordinate] | None = None, + n: int = 0, +) -> int: + if history is None: + history = set() + + if start_pos == end_pos: + return n + + history.add(start_pos) + adjacent = list(filter(lambda x: x.coordinate not in history, graph[start_pos])) + + if len(adjacent) == 0: + return -1 + + for edge in adjacent: + if edge.coordinate == end_pos: + return edge.distance + n + + return max( + trace_graph_path( + graph, p.coordinate, end_pos, history=history.copy(), n=n + p.distance + ) + for p in adjacent + ) + + +def two(instr: str): + forest = parse(instr) + start_pos, end_pos = get_terminal_points(forest) + graph = build_graph(forest, start_pos) + # display_graph(graph) + return trace_graph_path(graph, start_pos, end_pos) + + +def _debug(*args, **kwargs): + kwargs["file"] = sys.stderr + print(*args, **kwargs) + + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] not in ["1", "2"]: + print("Missing day argument", file=sys.stderr) + sys.exit(1) + inp = sys.stdin.read().strip() + if sys.argv[1] == "1": + print(one(inp)) + else: + print(two(inp)) diff --git a/challenges/2023/23-aLongWalk/tests.json b/challenges/2023/23-aLongWalk/tests.json new file mode 100644 index 0000000..71c7bb5 --- /dev/null +++ b/challenges/2023/23-aLongWalk/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "94", + "input": "#.#####################\n#.......#########...###\n#######.#########.#.###\n###.....#.>.>.###.#.###\n###v#####.#v#.###.#.###\n###.>...#.#.#.....#...#\n###v###.#.#.#########.#\n###...#.#.#.......#...#\n#####.#.#.#######.#.###\n#.....#.#.#.......#...#\n#.#####.#.#.#########v#\n#.#...#...#...###...>.#\n#.#.#v#######v###.###v#\n#...#.>.#...>.>.#.###.#\n#####v#.#.###v#.#.###.#\n#.....#...#...#.#.#...#\n#.#########.###.#.#.###\n#...###...#...#...#.###\n###.###.#.###v#####v###\n#...#...#.#.>.>.#.>.###\n#.###.###.#.###.#.#v###\n#.....###...###...#...#\n#####################.#\n\n" + } + ], + "2": [ + { + "is": "154", + "input": "#.#####################\n#.......#########...###\n#######.#########.#.###\n###.....#.>.>.###.#.###\n###v#####.#v#.###.#.###\n###.>...#.#.#.....#...#\n###v###.#.#.#########.#\n###...#.#.#.......#...#\n#####.#.#.#######.#.###\n#.....#.#.#.......#...#\n#.#####.#.#.#########v#\n#.#...#...#...###...>.#\n#.#.#v#######v###.###v#\n#...#.>.#...>.>.#.###.#\n#####v#.#.###v#.#.###.#\n#.....#...#...#.#.#...#\n#.#########.###.#.#.###\n#...###...#...#...#.###\n###.###.#.###v#####v###\n#...#...#.#.>.>.#.>.###\n#.###.###.#.###.#.#v###\n#.....###...###...#...#\n#####################.#\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/README.md b/challenges/2023/README.md index 327e4d5..f7a45b9 100644 --- a/challenges/2023/README.md +++ b/challenges/2023/README.md @@ -34,3 +34,4 @@ A day denoted with a star means it has a visualisation. | 20 - Pulse Propagation | ☆ ☆ | Python | Too much reading. | | 21 - Step Counter | ★ ☆ | Python | ??? | | 22 - Sand Slabs | ★ ★ | Python | I maintain that OpenSCAD is the best AoC 3D debugging tool | +| 23 - A Long Walk | ★ ★ | Python | Both parts here could theorietcially be done with the same implementation but I couldn't be bothered to rework the part 2 solution to work for part 1 as well. | diff --git a/challenges/2023/benchmark-graph.png b/challenges/2023/benchmark-graph.png index d0e6849..b056191 100644 Binary files a/challenges/2023/benchmark-graph.png and b/challenges/2023/benchmark-graph.png differ diff --git a/challenges/2023/benchmarks.jsonl b/challenges/2023/benchmarks.jsonl index a85fb08..a7717a2 100644 --- a/challenges/2023/benchmarks.jsonl +++ b/challenges/2023/benchmarks.jsonl @@ -39,3 +39,5 @@ {"day": 19, "part": 2, "runner": "py", "min": 0.024805068969726562, "max": 0.03318047523498535, "avg": 0.027637341022491456, "n": 100} {"day": 22, "part": 1, "runner": "py", "min": 7.400631666183472, "max": 7.400631666183472, "avg": 7.400631666183472, "n": 1} {"day": 22, "part": 2, "runner": "py", "min": 30.386138439178467, "max": 30.386138439178467, "avg": 30.386138439178467, "n": 1} +{"day": 23, "part": 1, "runner": "py", "min": 2.006169557571411, "max": 2.006169557571411, "avg": 2.006169557571411, "n": 1} +{"day": 23, "part": 2, "runner": "py", "min": 33.72923398017883, "max": 33.72923398017883, "avg": 33.72923398017883, "n": 1} diff --git a/gridutil/coord.py b/gridutil/coord.py index f8b4563..9c6605d 100644 --- a/gridutil/coord.py +++ b/gridutil/coord.py @@ -73,6 +73,8 @@ class Direction(Enum): return False def __eq__(self, x): + if type(x) != Direction: + return False return self.value == x.value def __hash__(self):