Add 2021-09 in Python (with visualisation)

This commit is contained in:
akp 2021-12-09 21:26:08 +00:00
parent 8c30110e64
commit cd23a43f53
No known key found for this signature in database
GPG key ID: AA5726202C8879B7
9 changed files with 268 additions and 1 deletions

View file

@ -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=<path to repo root>/lib python3 -B -c "import py;inp=open('input.txt').read();py.Challenge.vis(inp,'visdir')"
```

View file

@ -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
}

View file

@ -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"
}
]
}
}

Binary file not shown.

View file

@ -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)

View file

@ -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. |
<!-- PARSE END -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Before After
Before After

View file

@ -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)

12
lib/aocpy/vis.py Normal file
View file

@ -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