From 6c0337ad85333d7cd3252fbcbc5ac8af284806cf Mon Sep 17 00:00:00 2001 From: alexo Date: Sun, 24 Dec 2023 00:17:58 +1100 Subject: [PATCH] day23: part2 find edges --- day23/day23.py | 94 ++++---------------------- day23/lib/classes.py | 106 +++++++++++++++++++++++++++++- day23/lib/classes2.py | 135 ++++++++++++++++++++++++++++++++++++++ day23/lib/parsers.py | 4 +- day23/tests/test_day23.py | 5 +- 5 files changed, 255 insertions(+), 89 deletions(-) create mode 100644 day23/lib/classes2.py diff --git a/day23/day23.py b/day23/day23.py index bdc34e7..5457c5d 100644 --- a/day23/day23.py +++ b/day23/day23.py @@ -1,93 +1,22 @@ -from queue import Queue -from typing import Optional - -from day23.lib.classes import Maze, Path, Position +from day23.lib.classes import Maze, Path, Solver, Solver1 +from day23.lib.classes2 import Solver2 from day23.lib.parsers import get_maze INPUT = "day23/input.txt" INPUT_SMALL = "day23/input-small.txt" -class Solver: - maze: Maze - handle_hills: bool - - def __init__(self, maze: Maze, handle_hills: bool = True) -> None: - self.maze = maze - self.handle_hills = handle_hills - - def solve(self) -> list[Path]: - paths: Queue[Path] = Queue() - first_path = Path() - first_path.add(Position(0, 1)) - paths.put(first_path) - # bfs all paths simultaneously - results: list[Path] = [] - while not paths.empty(): - path = paths.get() - if path.last().row == self.maze.num_rows - 1: - results.append(path) - continue - - for expansion in self.expand_path(path): - paths.put(expansion) - return results - - def expand_hill(self, position: Position, tile: str) -> list[Position]: - if tile == "^": - return [position.copy_modify(row=-1)] - elif tile == "v": - return [position.copy_modify(row=+1)] - elif tile == "<": - return [position.copy_modify(col=-1)] - elif tile == ">": - return [position.copy_modify(col=+1)] - else: - return position.expand() - - def expand_path(self, path: Path) -> list[Path]: - current_pos: Position = path.last() - current_tile: Optional[str] = self.maze[current_pos] - if current_tile is None: - raise ValueError("there's no shot we got a tile outside the maze") - expansions: list[Position] - if self.handle_hills: - expansions = self.expand_hill(current_pos, current_tile) - else: - expansions = current_pos.expand() - - valid_expansions = [] - for expansion in expansions: - expansion_tile = self.maze[expansion] - if ( - path.can_add(expansion) - and expansion_tile is not None - and expansion_tile != "#" - ): - valid_expansions.append(expansion) - - if len(valid_expansions) == 0: - return [] - elif len(valid_expansions) == 1: - path.add(valid_expansions[0]) - return [path] - else: - result = [] - for expansion in valid_expansions[1:]: - new_path = path.copy() - new_path.add(expansion) - result.append(new_path) - path.add(valid_expansions[0]) - result.append(path) - return result +def part1(maze: Maze) -> int: + solver = Solver1(maze, True) + return run_solver(solver) -def part1(maze: Maze) -> int: - return solve(maze, True) +def part2(maze: Maze) -> int: + solver = Solver2(maze) + return run_solver(solver) -def solve(maze: Maze, handle_hills: bool) -> int: - solver = Solver(maze, handle_hills) +def run_solver(solver: Solver) -> int: paths: list[Path] = solver.solve() path_lengths = [len(path) for path in paths] path_lengths.sort(reverse=True) @@ -95,9 +24,10 @@ def solve(maze: Maze, handle_hills: bool) -> int: def main() -> None: - maze: Maze = get_maze(INPUT) + maze: Maze = get_maze(INPUT_SMALL) - print(part1(maze)) + # print(part1(maze)) + print(part2(maze)) if __name__ == "__main__": diff --git a/day23/lib/classes.py b/day23/lib/classes.py index 663a338..21a30ab 100644 --- a/day23/lib/classes.py +++ b/day23/lib/classes.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from queue import Queue from typing import Optional @@ -55,6 +56,11 @@ def copy(self) -> "Path": result.nodes = set(result.route) return result + def flip(self) -> "Path": + result = self.copy() + result.route.reverse() + return result + def last(self) -> Position: if len(self.route) == 0: raise ValueError("Don't call last when i'm empty 4head") @@ -74,17 +80,17 @@ def __len__(self) -> int: class Maze: - grid: list[str] # 2d array of chars + grid: list[list[str]] # 2d array of chars num_rows: int num_cols: int - def __init__(self, data: list[str]) -> None: + def __init__(self, data: list[list[str]]) -> None: self.grid = data self.num_rows = len(data) self.num_cols = len(data[0]) def __str__(self) -> str: - return "\n".join(row for row in self.grid) + return "\n".join("".join(col for col in row) for row in self.grid) def __getitem__(self, position: Position) -> Optional[str]: """Get item via position. Returns None if out of bounds""" @@ -94,6 +100,14 @@ def __getitem__(self, position: Position) -> Optional[str]: return None return self.grid[position.row][position.col] + def __setitem__(self, position: Position, value: str) -> None: + """Get item via position. Returns None if out of bounds""" + if not isinstance(position, Position): + raise ValueError(f"position is not a Position, {type(position)}") + if self.is_oob(position): + raise ValueError("can't set outside our maze!") + self.grid[position.row][position.col] = value + def is_oob(self, position: Position) -> bool: """true if position is out of bounds""" return ( @@ -102,3 +116,89 @@ def is_oob(self, position: Position) -> bool: or position.col < 0 or position.col >= self.num_cols ) + + +class Solver: + def solve(self) -> list[Path]: + return [] + + +class Solver1(Solver): + maze: Maze + handle_hills: bool + + def __init__(self, maze: Maze, handle_hills: bool = True) -> None: + self.maze = maze + self.handle_hills = handle_hills + + def solve(self) -> list[Path]: + paths: Queue[Path] = Queue() + first_path = Path() + first_path.add(Position(0, 1)) + paths.put(first_path) + # bfs all paths simultaneously + results: list[Path] = [] + count = 1 + while not paths.empty(): + path = paths.get() + if path.last().row == self.maze.num_rows - 1: + results.append(path) + continue + + expansions = self.expand_path(path) + for expansion in expansions: + paths.put(expansion) + count += 1 + if count % 1000 == 0: + print(count) + + return results + + def expand_hill(self, position: Position, tile: str) -> list[Position]: + if tile == "^": + return [position.copy_modify(row=-1)] + elif tile == "v": + return [position.copy_modify(row=+1)] + elif tile == "<": + return [position.copy_modify(col=-1)] + elif tile == ">": + return [position.copy_modify(col=+1)] + else: + return position.expand() + + def expand_path(self, path: Path) -> list[Path]: + current_pos: Position = path.last() + current_tile: Optional[str] = self.maze[current_pos] + if current_tile is None: + raise ValueError("there's no shot we got a tile outside the maze") + expansions: list[Position] + if self.handle_hills: + expansions = self.expand_hill(current_pos, current_tile) + else: + expansions = current_pos.expand() + + valid_expansions = [] + for expansion in expansions: + expansion_tile = self.maze[expansion] + if ( + path.can_add(expansion) + and expansion_tile is not None + and expansion_tile != "#" + ): + valid_expansions.append(expansion) + + if len(valid_expansions) == 0: + return [] + elif len(valid_expansions) == 1: + path.add(valid_expansions[0]) + return [path] + else: + result = [] + for expansion in valid_expansions[1:]: + new_path = path.copy() + new_path.add(expansion) + result.append(new_path) + path.add(valid_expansions[0]) + result.append(path) + + return result diff --git a/day23/lib/classes2.py b/day23/lib/classes2.py new file mode 100644 index 0000000..9a759b1 --- /dev/null +++ b/day23/lib/classes2.py @@ -0,0 +1,135 @@ +"""part 2 solution""" +from dataclasses import dataclass, field +from queue import Queue + +import colorama + +from day23.lib.classes import Maze, Path, Position, Solver + + +# first simplify graph to "nodes", then brute force that. +@dataclass +class Node: + name: int + position: Position + edges: list["Edge"] = field(default_factory=list) + + def __str__(self) -> str: + return f"{self.name}: ({self.position}) {[str(edge) for edge in self.edges]}" + + +@dataclass +class Edge: + node1: int + node2: int + path: Path = field(repr=False) + + def flip(self) -> "Edge": + return Edge(self.node2, self.node1, self.path.flip()) + + def __str__(self) -> str: + return f"{self.node1}->{self.node2}, {len(self.path)}" + + +class Solver2(Solver): + maze: Maze + + def __init__(self, maze: Maze) -> None: + self.maze = maze + + def get_cell_branches(self, position: Position) -> int: + result = 0 + if self.maze[position] != ".": + return 0 + for direction in position.expand(): + tile = self.maze[direction] + if tile is not None and tile != "#": + result += 1 + return result + + def get_nodes(self) -> dict[Position, Node]: + nodes: list[Node] = [] + + start = Position(0, 1) + nodes.append(Node(0, start)) + name = 1 + for row in range(self.maze.num_rows): + for col in range(self.maze.num_cols): + pos = Position(row, col) + if self.get_cell_branches(pos) > 2: + node = Node(name, pos) + name += 1 + nodes.append(node) + + # add start and end coz they are dumb + + end = Position(self.maze.num_rows - 1, self.maze.num_cols - 2) + + nodes.append(Node(name, end)) + for node in nodes: + self.maze[node.position] = ( + colorama.Back.GREEN + str(node.name) + colorama.Back.BLACK + ) + return {node.position: node for node in nodes} + + def fill_node(self, start_node: Node, nodes: dict[Position, Node]) -> None: + first_path = Path() + first_path.add(start_node.position) + paths: Queue[Path] = Queue() + paths.put(first_path) + while not paths.empty(): + path = paths.get() + pos = path.last() + if pos != start_node.position and pos in nodes: + # reached an edge + edge = Edge(start_node.name, nodes[pos].name, path) + start_node.edges.append(edge) + end_node = nodes[pos] + end_node.edges.append(edge.flip()) + continue + expansions = self.expand_path(path) + for path in expansions: + paths.put(path) + + def expand_path(self, path: Path) -> list[Path]: + current_pos: Position = path.last() + expansions = current_pos.expand() + + valid_expansions = [] + for expansion in expansions: + expansion_tile = self.maze[expansion] + if ( + path.can_add(expansion) + and expansion_tile is not None + and expansion_tile != "#" + ): + valid_expansions.append(expansion) + if expansion_tile == ".": + self.maze[expansion] = "#" + + if len(valid_expansions) == 0: + return [] + elif len(valid_expansions) == 1: + path.add(valid_expansions[0]) + return [path] + else: + result = [] + for expansion in valid_expansions[1:]: + new_path = path.copy() + new_path.add(expansion) + result.append(new_path) + path.add(valid_expansions[0]) + result.append(path) + + return result + + def solve(self) -> list[Path]: + nodes: dict[Position, Node] = self.get_nodes() + print(self.maze) + for node in nodes.values(): + self.fill_node(node, nodes) + + for node in nodes.values(): + print(node) + + return [] diff --git a/day23/lib/parsers.py b/day23/lib/parsers.py index 89b759b..47be223 100644 --- a/day23/lib/parsers.py +++ b/day23/lib/parsers.py @@ -2,7 +2,7 @@ def get_maze(path: str) -> Maze: - rows: list[str] = [] + rows: list[list[str]] = [] with open(path, encoding="utf8") as file: - rows = [line.strip() for line in file] + rows = [list(line.strip()) for line in file] return Maze(rows) diff --git a/day23/tests/test_day23.py b/day23/tests/test_day23.py index 0f381b9..6208a82 100644 --- a/day23/tests/test_day23.py +++ b/day23/tests/test_day23.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING -from day23.day23 import INPUT_SMALL, Solver, part1 +from day23.day23 import INPUT_SMALL, part1 +from day23.lib.classes import Solver1 from day23.lib.parsers import get_maze if TYPE_CHECKING: @@ -10,7 +11,7 @@ def test_solver() -> None: maze: Maze = get_maze(INPUT_SMALL) - solver = Solver(maze) + solver = Solver1(maze) paths: list[Path] = solver.solve() path_lengths = [len(path) for path in paths]