diff --git a/challenges/2021/09-smokeBasin/README.md b/challenges/2021/09-smokeBasin/README.md new file mode 100644 index 0000000..18e69de --- /dev/null +++ b/challenges/2021/09-smokeBasin/README.md @@ -0,0 +1,29 @@ +# [Day 9: Smoke Basin](https://adventofcode.com/2021/day/9) + +The key to solving part two is knowing that you don't need to itelligently think about the depths of the basins. + +Because we know that + +> Locations of height `9` do not count as being in any basin, and all other locations will always be part of exactly one basin + +we can determine that a given basin is bounded by `9`s or the edge of the board. For example, the sample input looks like this if you remove all digits apart from `9`. + +``` +--999----- +-9---9-9-- +9-----9-9- +-----9---9 +9-999----- +``` + +We can find every lowest point, and work outwards from that point to find the entire basin. + +--- + +[Part two visualisation](partTwo.mp4) + +To run the visualisation, use the following command: + +``` +PYTHONPATH=/lib python3 -B -c "import py;inp=open('input.txt').read();py.Challenge.vis(inp,'visdir')" +``` \ No newline at end of file diff --git a/challenges/2021/09-smokeBasin/benchmark.json b/challenges/2021/09-smokeBasin/benchmark.json new file mode 100644 index 0000000..1021e74 --- /dev/null +++ b/challenges/2021/09-smokeBasin/benchmark.json @@ -0,0 +1,15 @@ +{ + "day": 9, + "dir": "challenges/2021/09-smokeBasin", + "implementations": { + "Python": { + "part.1.avg": 0.04718727922439575, + "part.1.max": 0.19989562034606934, + "part.1.min": 0.01859116554260254, + "part.2.avg": 0.10986182379722595, + "part.2.max": 0.3891136646270752, + "part.2.min": 0.041121721267700195 + } + }, + "numRuns": 1000 +} \ No newline at end of file diff --git a/challenges/2021/09-smokeBasin/info.json b/challenges/2021/09-smokeBasin/info.json new file mode 100644 index 0000000..8cc1f54 --- /dev/null +++ b/challenges/2021/09-smokeBasin/info.json @@ -0,0 +1,17 @@ +{ + "inputFile": "input.txt", + "testCases": { + "one": [ + { + "input": "2199943210\n3987894921\n9856789892\n8767896789\n9899965678\n", + "expected": "15" + } + ], + "two": [ + { + "input": "2199943210\n3987894921\n9856789892\n8767896789\n9899965678\n", + "expected": "1134" + } + ] + } +} \ No newline at end of file diff --git a/challenges/2021/09-smokeBasin/partTwo.mp4 b/challenges/2021/09-smokeBasin/partTwo.mp4 new file mode 100644 index 0000000..091551b Binary files /dev/null and b/challenges/2021/09-smokeBasin/partTwo.mp4 differ diff --git a/challenges/2021/09-smokeBasin/py/__init__.py b/challenges/2021/09-smokeBasin/py/__init__.py new file mode 100644 index 0000000..2623870 --- /dev/null +++ b/challenges/2021/09-smokeBasin/py/__init__.py @@ -0,0 +1,180 @@ +from typing import List, Dict, Tuple +from aocpy import BaseChallenge, foldl + + +Point = Tuple[int, int] +Cave = Dict[Point, int] +Basin = List[Point] + + +def parse(instr: str) -> Cave: + o = {} + for y, line in enumerate( + instr.strip().splitlines() + ): + for x, digit in enumerate(line): + o[(x, y)] = int(digit) + return o + + +def find_adjacent_points(cave: Cave, position: Point) -> List[Point]: + # Returns a list of points that are horizontally or vertically adjacent to + # `point` that exist within `cave`. + + # (x, y+1) + # (x-1, y) (x, y) (x+1, y) + # (x, y-1) + # + + x, y = position + return [pos for pos in [(x, y-1), (x-1, y), (x+1, y), (x, y+1)] if pos in cave] + +def find_adjacent_heights(cave: Cave, position: Point) -> List[int]: + # Returns a list of heights of points horizontally or vertically adjacent to + # `point` + + return [cave[pos] for pos in find_adjacent_points(cave, position) if pos in cave] + + +def find_low_points(cave: Cave) -> List[Point]: + # Retusn a list of points in `cave` where all adjacent points are higher + # than the current point under inspection. + + o = [] + for position in cave: + height = cave[position] + are_adjacent_heights_higher = [height < adjacent for adjacent in find_adjacent_heights(cave, position)] + if False not in are_adjacent_heights_higher: + o.append(position) + return o + + +def find_points_in_basin(cave: Cave, low_point: Point) -> List[Point]: + # Returns a list of points that belong to the basin in `cave` that's + # centered around `low_point` + + queue = find_adjacent_points(cave, low_point) + points = [low_point] + + while len(queue) != 0: + point = queue.pop(0) + + if cave[point] == 9: + continue + + points.append(point) + for adjacent in find_adjacent_points(cave, point): + if adjacent in queue or adjacent in points: + continue + queue.append(adjacent) + + return points + + +def find_basins(cave: Cave) -> List[Basin]: + # Returns all the basins that exist within `cave` + + low_points = find_low_points(cave) + basins = [] + for point in low_points: + basins.append(find_points_in_basin(cave, point)) + return basins + + +class Challenge(BaseChallenge): + + @staticmethod + def one(instr: str) -> int: + cave = parse(instr) + + cumulative_risk_level = 0 + for low_point in find_low_points(cave): + cumulative_risk_level += cave[low_point] + 1 + + return cumulative_risk_level + + @staticmethod + def two(instr: str) -> int: + cave = parse(instr) + basins = find_basins(cave) + + # reverse == descending order + basins = list(sorted(basins, key=lambda x: len(x), reverse=True)) + + return foldl(lambda x, y: x*len(y), basins[0:3], 1) + + @staticmethod + def vis(instr: str, outputDir: str): + from PIL import Image, ImageDraw + import os + from aocpy.vis import SaveManager + import shutil + + COLOUR_BACKGROUND = tuple(bytes.fromhex("FFFFFF")) + COLOUR_SCANNING = tuple(bytes.fromhex("D4F1F4")) + COLOUR_LOW_POINT = tuple(bytes.fromhex("05445E")) + COLOUR_BORDER = tuple(bytes.fromhex("189AB4")) + COLOUR_BASIN = tuple(bytes.fromhex("75E6DA")) + + cave = parse(instr) + + max_x = max(x for x, _ in cave) + max_y = max(y for _, y in cave) + + img = Image.new("RGB", (max_x + 1, max_y + 1), COLOUR_BACKGROUND) + + temp_dir = os.path.join(outputDir, "vis-temp") + try: + os.makedirs(temp_dir) + except FileExistsError: + pass + + manager = SaveManager(temp_dir) + + # now we reimplement bits of the challenge + + def find_points_in_basin(cave: Cave, low_point: Point) -> List[Point]: + queue = find_adjacent_points(cave, low_point) + points = [low_point] + + while len(queue) != 0: + point = queue.pop(0) + + if cave[point] == 9: + img.putpixel(point, COLOUR_BORDER) + manager.save(img) + continue + + points.append(point) + img.putpixel(point, COLOUR_BASIN) + manager.save(img) + for adjacent in find_adjacent_points(cave, point): + if adjacent in queue or adjacent in points: + continue + queue.append(adjacent) + + return points + + def find_low_points(cave: Cave) -> List[Point]: + o = [] + for position in cave: + img.putpixel(position, COLOUR_SCANNING) + manager.save(img) + + height = cave[position] + are_adjacent_heights_higher = [height < adjacent for adjacent in find_adjacent_heights(cave, position)] + if False not in are_adjacent_heights_higher: + o.append(position) + img.putpixel(position, COLOUR_LOW_POINT) + else: + img.putpixel(position, COLOUR_BACKGROUND) + manager.save(img) + return o + + # actual challenge run + low_points = find_low_points(cave) + for point in low_points: + find_points_in_basin(cave, point) + + os.system(f"""ffmpeg -framerate 480 -i {outputDir}/vis-temp/frame_%04d.png -start_number 0 -c:v libx264 -vf "scale=iw*5:ih*5:flags=neighbor" -r 30 -pix_fmt yuv420p {outputDir}/out.mp4""") + shutil.rmtree(temp_dir) diff --git a/challenges/2021/README.md b/challenges/2021/README.md index 4c9b428..5903870 100644 --- a/challenges/2021/README.md +++ b/challenges/2021/README.md @@ -18,6 +18,7 @@ Solutions to the [2021 Advent of Code](https://adventofcode.com/2021). | 06 - Lanternfish | Complete | [Python](06-lanternfish/py) | At this rate, the mass of the fish would surpass that of the Earth pretty quickly. | | 07 - The Treachery of Whales | Complete | [Python](07-theTreacheryOfWhales/py) | I'm not 100% sure my solution for part two is valid for all possible inputs. | | 08 - Seven Segment Search | Complete | [Python](08-sevenSegmentSearch/py), [Go](08-sevenSegmentSearch) | I may have taken the easy way out for part two, but it does work! No-one ever said the smart solution is the best solution, anyway. | +| 09 - Smoke Basin * | Complete | [Python](09-smokeBasin/py) | Schmokey! Also, as it turns out, I struggle to implement basic logic. Fun. | diff --git a/challenges/2021/running-times.png b/challenges/2021/running-times.png index 9f812bd..2dd115e 100644 Binary files a/challenges/2021/running-times.png and b/challenges/2021/running-times.png differ diff --git a/lib/aocpy/__init__.py b/lib/aocpy/__init__.py index 9f0f7ed..3319f81 100644 --- a/lib/aocpy/__init__.py +++ b/lib/aocpy/__init__.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, TypeVar, Callable, Iterable class BaseChallenge: @@ -14,3 +14,16 @@ class BaseChallenge: @staticmethod def vis(instr: str, outputDir: str) -> Any: raise NotImplementedError + + +T = TypeVar("T") +U = TypeVar("U") + +def foldl(p: Callable[[U, T], U], i: Iterable[T], start: U) -> U: + res = start + for item in i: + res = p(res, item) + return res + +def foldr(p: Callable[[U, T], U], i: Iterable[T], start: U) -> U: + return foldl(p, reversed(i), start) \ No newline at end of file diff --git a/lib/aocpy/vis.py b/lib/aocpy/vis.py new file mode 100644 index 0000000..8d4a2d2 --- /dev/null +++ b/lib/aocpy/vis.py @@ -0,0 +1,12 @@ +import os.path + +class SaveManager(): + def __init__(self, d): + self.dir = d + self.current_n = 0 + + def save(self, im): + im.save(os.path.join(self.dir, f"frame_{str(self.current_n).zfill(4)}.png")) + self.current_n += 1 + + \ No newline at end of file