From ed5fda867fbb5e76e21bce75a357a62326ac5e3e Mon Sep 17 00:00:00 2001 From: AKP Date: Mon, 18 Dec 2023 17:54:05 +0000 Subject: [PATCH] 2023.18 --- challenges/2023/18-lavaductLagoon/README.md | 1 + challenges/2023/18-lavaductLagoon/main.py | 88 ++++++++++++++++++++ challenges/2023/18-lavaductLagoon/tests.json | 18 ++++ challenges/2023/README.md | 3 +- challenges/2023/benchmarks.jsonl | 2 + gridutil/coord.py | 33 ++++++-- gridutil/grid.py | 6 +- 7 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 challenges/2023/18-lavaductLagoon/README.md create mode 100644 challenges/2023/18-lavaductLagoon/main.py create mode 100644 challenges/2023/18-lavaductLagoon/tests.json diff --git a/challenges/2023/18-lavaductLagoon/README.md b/challenges/2023/18-lavaductLagoon/README.md new file mode 100644 index 0000000..c0320e5 --- /dev/null +++ b/challenges/2023/18-lavaductLagoon/README.md @@ -0,0 +1 @@ +# [Day 18: Lavaduct Lagoon](https://adventofcode.com/2023/day/18) diff --git a/challenges/2023/18-lavaductLagoon/main.py b/challenges/2023/18-lavaductLagoon/main.py new file mode 100644 index 0000000..b58752e --- /dev/null +++ b/challenges/2023/18-lavaductLagoon/main.py @@ -0,0 +1,88 @@ +import sys +import re +from collections import namedtuple +import gridutil.coord as cu +import gridutil.grid as gu + + +Instruction = namedtuple("Instruction", ["direction", "dist", "colour"]) +PARSE_RE = re.compile(r"([RDUL]) (\d+) \(#([a-f\d]{6})\)") +DIRECTION_TRANSFORMATION = { + "R": cu.Direction.Right, + "L": cu.Direction.Left, + "U": cu.Direction.Up, + "D": cu.Direction.Down, + "0": cu.Direction.Right, + "1": cu.Direction.Down, + "2": cu.Direction.Left, + "3": cu.Direction.Up, +} + + +def parse(instr: str) -> list[Instruction]: + res = [] + for line in instr.splitlines(): + m = PARSE_RE.match(line) + assert m is not None + + raw_dir, dist, colour = m.groups() + parsed_dir = DIRECTION_TRANSFORMATION[raw_dir] + assert parsed_dir is not None + + res.append(Instruction(parsed_dir, int(dist), colour)) + return res + + +def run(instructions: list[Instruction]) -> int: + perimeter = 0 + vertices = [cu.Coordinate(0, 0)] + for instruction in instructions: + perimeter += instruction.dist + vertices.append( + cu.add( + vertices[-1], cu.mult(instruction.direction.delta(), instruction.dist) + ) + ) + + vertices = vertices[:-1] + + area = cu.area(vertices) + + # This is Pick's theorem. + # Normally, we'd want to just get the internal area, which the Shoelace formula would do. + # But since we want the area including walls that we assume are a single + # unit thick, we apply Pick's theorem as this counts all coordinates that + # the walls pass through, which in this case is effectively the same thing. + return int(area + perimeter / 2) + 1 + + +def one(instr: str): + instructions = parse(instr) + return run(instructions) + + +def two(instr: str): + instructions = parse(instr) + for i, instruction in enumerate(instructions): + instructions[i] = Instruction( + DIRECTION_TRANSFORMATION[instruction.colour[-1]], + int(instruction.colour[:5], base=16), + "", + ) + return run(instructions) + + +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/18-lavaductLagoon/tests.json b/challenges/2023/18-lavaductLagoon/tests.json new file mode 100644 index 0000000..6870dea --- /dev/null +++ b/challenges/2023/18-lavaductLagoon/tests.json @@ -0,0 +1,18 @@ +{ + "1": [ + { + "is": "62", + "input": "R 6 (#70c710)\nD 5 (#0dc571)\nL 2 (#5713f0)\nD 2 (#d2c081)\nR 2 (#59c680)\nD 2 (#411b91)\nL 5 (#8ceee2)\nU 2 (#caa173)\nL 1 (#1b58a2)\nU 2 (#caa171)\nR 2 (#7807d2)\nU 3 (#a77fa3)\nL 2 (#015232)\nU 2 (#7a21e3)\n\n" + }, + { + "is": "20", + "input": "R 2 (#000000)\nD 1 (#000000)\nR 2 (#000000)\nU 1 (#000000)\nR 2 (#000000)\nD 2 (#000000)\nL 6 (#000000)\nU 2 (#000000)\n" + } + ], + "2": [ + { + "is": "952408144115", + "input": "R 6 (#70c710)\nD 5 (#0dc571)\nL 2 (#5713f0)\nD 2 (#d2c081)\nR 2 (#59c680)\nD 2 (#411b91)\nL 5 (#8ceee2)\nU 2 (#caa173)\nL 1 (#1b58a2)\nU 2 (#caa171)\nR 2 (#7807d2)\nU 3 (#a77fa3)\nL 2 (#015232)\nU 2 (#7a21e3)\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/README.md b/challenges/2023/README.md index f21be7d..a048a88 100644 --- a/challenges/2023/README.md +++ b/challenges/2023/README.md @@ -30,4 +30,5 @@ A day denoted with a star means it has a visualisation. | 14* - Parabolic Reflector Dish | ★ ★ | Python | Why do I always overcomplicate cycle detection?! | | 15 - Lens Library | ★ ★ | Go | Still took some brainpower but this time the brainpower was needed to work out what the problem was, *not* to work out how to solve the problem. | | 16 - The Floor Will Be Lava | ★ ★ | Python | Pathfinding, sort of, but also brute forceable?? | -| 17 - Clumsy Crucible | ★ ★ | Python | This taught me quite a lot about how to meddle with Djikstra's | \ No newline at end of file +| 17 - Clumsy Crucible | ★ ★ | Python | This taught me quite a lot about how to meddle with Djikstra's | +| 18 - Ladaduct Lagoon | ★ ★ | Python | Nothing quite like a problem that I thought I knew the solution to showing up my lack of mathematical knowledge. | \ No newline at end of file diff --git a/challenges/2023/benchmarks.jsonl b/challenges/2023/benchmarks.jsonl index 38b8d9d..9d425ec 100644 --- a/challenges/2023/benchmarks.jsonl +++ b/challenges/2023/benchmarks.jsonl @@ -31,3 +31,5 @@ {"day": 16, "part": 2, "runner": "py", "min": 2.7863943576812744, "max": 4.14529013633728, "avg": 3.1346225261688234, "n": 15} {"day": 17, "part": 1, "runner": "py", "min": 5.36311674118042, "max": 5.36311674118042, "avg": 5.36311674118042, "n": 1} {"day": 17, "part": 2, "runner": "py", "min": 26.201914072036743, "max": 26.201914072036743, "avg": 26.201914072036743, "n": 1} +{"day": 18, "part": 1, "runner": "py", "min": 0.02330160140991211, "max": 0.03203868865966797, "avg": 0.024628419876098633, "n": 100} +{"day": 18, "part": 2, "runner": "py", "min": 0.023529052734375, "max": 0.030207157135009766, "avg": 0.02483478546142578, "n": 100} diff --git a/gridutil/coord.py b/gridutil/coord.py index 04c7b54..c030b85 100644 --- a/gridutil/coord.py +++ b/gridutil/coord.py @@ -3,19 +3,24 @@ from collections import namedtuple from numbers import Number -Coordinate: tuple[Number, Number] = namedtuple("Coordinate", ["x", "y"]) +Coordinate = namedtuple("Coordinate", ["x", "y"]) def add(a: Coordinate, b: Coordinate) -> Coordinate: - return Coordinate(a.x + b.x, a.y + b.y) + xa, ya = a + xb, yb = b + return Coordinate(xa + xb, ya + yb) def sub(a: Coordinate, b: Coordinate) -> Coordinate: - return Coordinate(a.x - b.x, a.y - b.y) + xa, ya = a + xb, yb = b + return Coordinate(xa - xb, ya - yb) def mult(a: Coordinate, b: Number) -> Coordinate: - return Coordinate(a.x * b, a.y * b) + x, y = a + return Coordinate(x * b, y * b) def manhattan_dist(a: Coordinate, b: Coordinate) -> Number: @@ -23,6 +28,18 @@ def manhattan_dist(a: Coordinate, b: Coordinate) -> Number: return abs(x) + abs(y) +def area(x: list[Coordinate]) -> Number: + """ + Finds the area of a closed polygon. + + https://en.wikipedia.org/wiki/Shoelace_formula + """ + acc = 0 + for ((ax, ay), (bx, by)) in zip(x, x[1:] + [x[0]]): + acc += (ax * by) - (bx * ay) + return acc / 2 + + class Direction(Enum): Up = auto() Down = auto() @@ -32,13 +49,13 @@ class Direction(Enum): def delta(self) -> Coordinate: match self: case Direction.Up: - return (0, -1) + return Coordinate(0, -1) case Direction.Down: - return (0, 1) + return Coordinate(0, 1) case Direction.Left: - return (-1, 0) + return Coordinate(-1, 0) case Direction.Right: - return (1, 0) + return Coordinate(1, 0) def opposite(self): match self: diff --git a/gridutil/grid.py b/gridutil/grid.py index b264f92..e71c478 100644 --- a/gridutil/grid.py +++ b/gridutil/grid.py @@ -14,7 +14,7 @@ def parse(instr: str, filter_fn: Optional[Callable[[str], bool]] = None) -> Grid for y, line in enumerate(instr.splitlines()): for x, char in enumerate(line): if filter_fn(char): - res[(x, y)] = char + res[coord.Coordinate(x, y)] = char return res @@ -37,8 +37,8 @@ def get_max_y(grid: Grid, filter_fn: Optional[Callable[[T], bool]] = None) -> in def print_grid(grid: Grid, **kwargs): - for y in range(get_max_y(grid) + 1): - for x in range(get_max_x(grid) + 1): + for y in range(min(map(lambda x: x[1], grid)), get_max_y(grid) + 1): + for x in range(min(map(lambda x: x[0], grid)), get_max_x(grid) + 1): v = grid.get((x, y), " ") print(v, end="", **kwargs) print(**kwargs)