diff --git a/challenges/2023/22-sandSlabs/README.md b/challenges/2023/22-sandSlabs/README.md new file mode 100644 index 0000000..2cd6d89 --- /dev/null +++ b/challenges/2023/22-sandSlabs/README.md @@ -0,0 +1 @@ +# [Day 22: Sand Slabs](https://adventofcode.com/2023/day/22) diff --git a/challenges/2023/22-sandSlabs/main.py b/challenges/2023/22-sandSlabs/main.py new file mode 100644 index 0000000..357cc1f --- /dev/null +++ b/challenges/2023/22-sandSlabs/main.py @@ -0,0 +1,146 @@ +import sys +import gridutil.coord as cu +from tqdm import tqdm + + +Brick = tuple[cu.Coordinate3, cu.Coordinate3] + + +def parse(instr: str) -> set[Brick]: + # (sorry) + return set( + sorted( + [ + tuple( + sorted( + map( + lambda x: cu.Coordinate3(*map(int, x.split(","))), + line.split("~"), + ) + ) + ) + for line in instr.splitlines() + ], + key=lambda x: x[0].z, + ) + ) + + +def are_bricks_overlapping(a: Brick, b: Brick) -> bool: + x_overlaps = a[0].x <= b[1].x and b[0].x <= a[1].x + y_overlaps = a[0].y <= b[1].y and b[0].y <= a[1].y + return x_overlaps and y_overlaps + + +def lower_bricks( + bricks: set[Brick], start_at: int = 2, return_early: bool = False +) -> int: + max_z = max([x[1].z for x in bricks]) + 1 + n = 0 + for level in range(start_at, max_z if not return_early else start_at + 1): + present_below = [] + on_this_level = [] + + for brick in bricks: + if brick[1].z == level - 1: + present_below.append(brick) + elif brick[0].z == level: + on_this_level.append(brick) + + for brick in on_this_level: + overlaps = False + for lower_brick in present_below: + if are_bricks_overlapping(brick, lower_brick): + overlaps = True + break + + if not overlaps: + n += 1 + # This is what lowering a brick looks like + bricks.remove(brick) + bricks.add( + ( + cu.Coordinate3(brick[0].x, brick[0].y, brick[0].z - 1), + cu.Coordinate3(brick[1].x, brick[1].y, brick[1].z - 1), + ) + ) + + return n + + +def collapse_tower(bricks: set[Brick]): + changed = 1 + while changed != 0: + changed = lower_bricks(bricks) + + +def generate_openscad(bricks: set[Brick]) -> str: + """ + This was for debugging + """ + lines = [] + for brick in bricks: + lines.append( + "translate([" + + (", ".join(map(str, brick[0]))) + + "]) { cube([" + + ( + ", ".join( + map(str, map(lambda x: 1 + brick[1][x] - brick[0][x], range(3))) + ) + ) + + "], center=false); };" + ) + return "\n".join(lines) + + +def one(instr: str): + bricks = parse(instr) + + _debug("Building initial state", end="\r") + + collapse_tower(bricks) + + acc = 0 + for brick in tqdm(bricks, desc="Testing individual bricks", file=sys.stderr, leave=False): + bc = bricks.copy() + bc.remove(brick) + v = lower_bricks(bc, start_at=brick[1].z + 1, return_early=True) + if v == 0: + acc += 1 + + return acc + + +def two(instr: str): + bricks = parse(instr) + + _debug("Building initial state", end="\r") + + collapse_tower(bricks) + + acc = 0 + for brick in tqdm(bricks, desc="Testing individual bricks", file=sys.stderr, leave=False): + bc = bricks.copy() + bc.remove(brick) + v = lower_bricks(bc, start_at=brick[1].z + 1) + if v != 0: + acc += v + + return acc + + +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/22-sandSlabs/tests.json b/challenges/2023/22-sandSlabs/tests.json new file mode 100644 index 0000000..39ba35e --- /dev/null +++ b/challenges/2023/22-sandSlabs/tests.json @@ -0,0 +1,14 @@ +{ + "1": [ + { + "is": "5", + "input": "1,0,1~1,2,1\n0,0,2~2,0,2\n0,2,3~2,2,3\n0,0,4~0,2,4\n2,0,5~2,2,5\n0,1,6~2,1,6\n1,1,8~1,1,9\n\n" + } + ], + "2": [ + { + "is": "7", + "input": "1,0,1~1,2,1\n0,0,2~2,0,2\n0,2,3~2,2,3\n0,0,4~0,2,4\n2,0,5~2,2,5\n0,1,6~2,1,6\n1,1,8~1,1,9\n\n" + } + ] +} \ No newline at end of file diff --git a/challenges/2023/README.md b/challenges/2023/README.md index 34569c0..fcb76e6 100644 --- a/challenges/2023/README.md +++ b/challenges/2023/README.md @@ -35,3 +35,4 @@ A day denoted with a star means it has a visualisation. | 19 - Aplenty | ★ ★ | Python | So maybe I *can* do range maths? | | 20 - Pulse Propagation | ☆ ☆ | Python | Too much reading. | | 21 - Step Counter | ★ ☆ | Python | ??? | +| 21 - Sand Slabs | ★ ★ | Python | I maintain that OpenSCAD is the best AoC 3D debugging tool | diff --git a/challenges/2023/benchmark-graph.png b/challenges/2023/benchmark-graph.png index 0a71b7d..d0e6849 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 29de885..a85fb08 100644 --- a/challenges/2023/benchmarks.jsonl +++ b/challenges/2023/benchmarks.jsonl @@ -37,3 +37,5 @@ {"day": 19, "part": 2, "runner": "py", "min": 0.026041030883789062, "max": 0.03458356857299805, "avg": 0.028042428493499756, "n": 100} {"day": 19, "part": 1, "runner": "py", "min": 0.023969173431396484, "max": 0.03136777877807617, "avg": 0.026349050998687742, "n": 100} {"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} diff --git a/gridutil/coord.py b/gridutil/coord.py index c030b85..f8b4563 100644 --- a/gridutil/coord.py +++ b/gridutil/coord.py @@ -4,6 +4,7 @@ from numbers import Number Coordinate = namedtuple("Coordinate", ["x", "y"]) +Coordinate3 = namedtuple("Coordinate3", ["x", "y", "z"]) def add(a: Coordinate, b: Coordinate) -> Coordinate: