From 2cecebac87ee8b3b444168950ce1644a08490f8f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Tue, 20 May 2025 00:51:59 +0100 Subject: [PATCH] 2016: d22: ex2: add solution --- 2016/d22/ex2/ex2.py | 101 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100755 2016/d22/ex2/ex2.py diff --git a/2016/d22/ex2/ex2.py b/2016/d22/ex2/ex2.py new file mode 100755 index 0000000..1249ba3 --- /dev/null +++ b/2016/d22/ex2/ex2.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python + +import heapq +import sys +from collections.abc import Iterable +from typing import NamedTuple + + +class Point(NamedTuple): + x: int + y: int + + +class Filesystem(NamedTuple): + size: int + used: int + + @property + def avail(self) -> int: + return self.size - self.used + + +def solve(input: str) -> int: + def parse_line(input: str) -> tuple[Point, Filesystem]: + raw_fs, raw_size, raw_used, raw_avail, _ = input.split() + size = int(raw_size.removesuffix("T")) + used = int(raw_used.removesuffix("T")) + avail = int(raw_avail.removesuffix("T")) + assert size == (used + avail) # Sanity check + *_, x, y = raw_fs.split("-") + return Point(int(x[1:]), int(y[1:])), Filesystem(size, used) + + def parse(input: str) -> dict[Point, Filesystem]: + return {node: fs for node, fs in map(parse_line, input.splitlines()[2:])} + + def neighbours(p: Point) -> Iterable[Point]: + for dx, dy in ( + (-1, 0), + (1, 0), + (0, -1), + (0, 1), + ): + yield Point(p.x + dx, p.y + dy) + + def dijkstra(start: Point, end: Point, points: set[Point]) -> int: + # Priority queue of (distance, point) + queue = [(0, start)] + seen: set[Point] = set() + + while len(queue) > 0: + dist, p = heapq.heappop(queue) + if p == end: + return dist + # We must have seen p with a smaller distance before + if p in seen: + continue + # First time encountering p, must be the smallest distance to it + seen.add(p) + # Add all neighbours to be visited + for n in neighbours(p): + if n not in points: + continue + heapq.heappush(queue, (dist + 1, n)) + + assert False # Sanity check + + def dist(p: Point, other: Point) -> int: + return abs(p.x - other.x) + abs(p.y - other.y) + + def has_angle(p: Point, other: Point) -> int: + return p.x != other.x and p.y != other.y + + def data_migration(start: Point, end: Point) -> int: + # Moving the data once is a 5 step move, unless it is on an angle, where it is 3 + # Assumes there's no wall between either points + return 5 * dist(start, end) - 2 * has_angle(start, end) + + df = parse(input) + # The data moves from the goal to us, hence `start` and `end` are "reversed" + start = max(p for p in df.keys() if p.y == 0) + end = Point(0, 0) + # The "hole" must be used to migrate the data to us + hole = next(p for p, fs in df.items() if fs.used == 0) + # The "walls" are nodes which are too big to move, effectively blocking us + walls = {p for p, fs in df.items() if fs.used > df[hole].avail} + accessible = df.keys() - walls + + return min( + dijkstra(hole, n, accessible) + 1 + data_migration(n, end) + for n in neighbours(start) + if n in accessible + ) + + +def main() -> None: + input = sys.stdin.read() + print(solve(input)) + + +if __name__ == "__main__": + main()