Add 2021-09 in Python (with visualisation)
This commit is contained in:
parent
8c30110e64
commit
cd23a43f53
9 changed files with 268 additions and 1 deletions
29
challenges/2021/09-smokeBasin/README.md
Normal file
29
challenges/2021/09-smokeBasin/README.md
Normal 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')"
|
||||||
|
```
|
15
challenges/2021/09-smokeBasin/benchmark.json
Normal file
15
challenges/2021/09-smokeBasin/benchmark.json
Normal 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
|
||||||
|
}
|
17
challenges/2021/09-smokeBasin/info.json
Normal file
17
challenges/2021/09-smokeBasin/info.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
challenges/2021/09-smokeBasin/partTwo.mp4
Normal file
BIN
challenges/2021/09-smokeBasin/partTwo.mp4
Normal file
Binary file not shown.
180
challenges/2021/09-smokeBasin/py/__init__.py
Normal file
180
challenges/2021/09-smokeBasin/py/__init__.py
Normal 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)
|
|
@ -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. |
|
| 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. |
|
| 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. |
|
| 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 -->
|
<!-- PARSE END -->
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 53 KiB |
|
@ -1,4 +1,4 @@
|
||||||
from typing import Any
|
from typing import Any, TypeVar, Callable, Iterable
|
||||||
|
|
||||||
|
|
||||||
class BaseChallenge:
|
class BaseChallenge:
|
||||||
|
@ -14,3 +14,16 @@ class BaseChallenge:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def vis(instr: str, outputDir: str) -> Any:
|
def vis(instr: str, outputDir: str) -> Any:
|
||||||
raise NotImplementedError
|
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
12
lib/aocpy/vis.py
Normal 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
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue