This commit is contained in:
akp 2022-12-17 21:20:50 +00:00
parent c1505bb80f
commit 884a90b551
No known key found for this signature in database
GPG key ID: AA5726202C8879B7
9 changed files with 326 additions and 18 deletions

View file

@ -0,0 +1 @@
# [Day 17: Pyroclastic Flow](https://adventofcode.com/2022/day/17)

View file

@ -0,0 +1,15 @@
{
"day": 17,
"dir": "challenges/2022/17-pyroclasticFlow",
"implementations": {
"Python": {
"part.1.avg": 0.5983425378799438,
"part.1.max": 0.6658821105957031,
"part.1.min": 0.5308029651641846,
"part.2.avg": 12.261357307434082,
"part.2.max": 12.877821207046509,
"part.2.min": 11.644893407821655
}
},
"numRuns": 2
}

View file

@ -0,0 +1,17 @@
{
"inputFile": "input.txt",
"testCases": {
"one": [
{
"input": ">>><<><>><<<>><>>><<<>>><<<><<<>><>><<>>",
"expected": "3068"
}
],
"two": [
{
"input": ">>><<><>><<<>><>>><<<>>><<<><<<>><>><<>>",
"expected": "1514285714288"
}
]
}
}

View file

@ -0,0 +1,233 @@
from typing import *
from aocpy import BaseChallenge, Vector, RepeatingConsumer, foldl
Tube = Dict[Vector, None]
Rock = Tuple[int, int, int, int, int]
TUBE_WIDTH = 7
# Coordinate system:
# ^
# |
# x --------->
# |
# y
def parse_rock(x: str) -> Tuple[Tuple[Vector], Tuple[Vector]]:
res = []
for y, line in enumerate(reversed(x.strip().splitlines())):
for x, char in enumerate(line):
if char == "#":
res.append(Vector(x, y))
height = y + 1
width = x + 1
left = []
right = []
for y in range(height):
cx = tuple(coord.x for coord in res if coord.y == y)
min_x = min(cx)
max_x = max(cx)
left.append(Vector(min_x, y))
right.append(Vector(max_x, y))
below = []
for x in range(width):
min_y = min(coord.y for coord in res if coord.x == x)
below.append(Vector(x, min_y - 1))
return tuple(res), tuple(left), tuple(right), tuple(below), width
ROCKS: List[Rock] = [
parse_rock(lines)
for lines in "####\n\n.#.\n###\n.#.\n\n..#\n..#\n###\n\n#\n#\n#\n#\n\n##\n##".split(
"\n\n"
)
]
def get_num_rows(tube: Tube) -> int:
filled_spaces = tube.keys()
return max(x.y for x in filled_spaces) + 1 if len(filled_spaces) != 0 else 0
def get_rock_position(
rock: Rock, instructions: RepeatingConsumer, tube: Tube
) -> Vector:
(_, slots_left, slots_right, slots_below, rock_width) = rock
filled_spaces = tube.keys()
position = Vector(2, get_num_rows(tube) + 3)
while True:
shift_direction = instructions.take()
delta_x = 0
if shift_direction == "<":
if position.x - 1 >= 0:
delta_x = -1
elif shift_direction == ">":
if position.x + rock_width < TUBE_WIDTH:
# rock_width includes a +1 for our needs
delta_x = 1
else:
raise ValueError("invalid shift direction")
if delta_x != 0:
slots = slots_left if delta_x < 0 else slots_right
can_move = foldl(
lambda x, y: x and y,
(
((p.x + position.x + delta_x, p.y + position.y) not in tube)
for p in slots
),
True,
)
if can_move:
position.x += delta_x
can_move_down = False
if position.y != 0:
can_move_down = foldl(
lambda x, y: x and y,
(
((p.x + position.x, p.y + position.y) not in tube)
for p in slots_below
),
True,
)
if not can_move_down:
break
position.y -= 1
return position
def add_rock(rock: Rock, position: Vector, tube: Tube):
rock_shape = rock[0]
for p in rock_shape:
tube[Vector(position.x + p.x, position.y + p.y)] = None
def get_row_bitmap(tube: Tube, y: int) -> int:
n = 0
for i in range(TUBE_WIDTH):
n = n << 1
n += 1 if (i, y) in tube else 0
return n
def check_for_sequences(tube: Tube) -> Optional[Tuple[int, int]]:
rows = get_num_rows(tube)
segment_size = rows // 2
if segment_size % 100 == 0:
print(f"starting segment size {segment_size}", flush=True)
while segment_size > 30:
possible_positions = (rows - (segment_size * 2)) + 1
for i in range(possible_positions):
continuous = True
for n in range(segment_size):
for x in range(TUBE_WIDTH):
if (x, i + n) in tube != (x, i + n + segment_size) in tube:
continuous = False
break
if not continuous:
break
if continuous:
return segment_size, i
segment_size -= 1
return None
def count_rocks_to_height(
tube: Tube, rocks: RepeatingConsumer, instructions: RepeatingConsumer, height: int
) -> int:
n = 0
while get_num_rows(tube) != height:
rock = rocks.take()
pos = get_rock_position(rock, instructions, tube)
add_rock(rock, pos, tube)
n += 1
return n
def get_heights(tube: Tube) -> Tuple[Tuple[int], int]:
max_height = get_num_rows(tube)
res = []
for x in range(TUBE_WIDTH):
largest = 0
for v in (p.y for p in tube if p.x == x):
if v > largest:
largest = v
res.append(max_height - largest)
return tuple(res), max_height
class Challenge(BaseChallenge):
@staticmethod
def one(instr: str) -> int:
tube: Tube = {}
instructions = RepeatingConsumer(instr.strip())
rocks = RepeatingConsumer(ROCKS)
for _ in range(2022):
rock = rocks.take()
pos = get_rock_position(rock, instructions, tube)
add_rock(rock, pos, tube)
return get_num_rows(tube)
@staticmethod
def two(instr: str) -> int:
tube: Tube = {}
instructions = RepeatingConsumer(instr.strip())
rocks = RepeatingConsumer(ROCKS)
seen: Dict[int, int] = {}
target = 1000000000000
num_rocks_thrown = 0
while num_rocks_thrown < target:
rock = rocks.take()
pos = get_rock_position(rock, instructions, tube)
add_rock(rock, pos, tube)
col_heights, max_height = get_heights(tube)
h = (rocks.i, instructions.i, *col_heights)
if h in seen:
cycle_begins, height_at_cycle_begin = seen[h]
rocks_thrown_per_cycle = num_rocks_thrown - cycle_begins
height_increase_per_cycle = max_height - height_at_cycle_begin
rocks_left_to_throw = target - num_rocks_thrown
full_cycles_remaining = rocks_left_to_throw // rocks_thrown_per_cycle
num_rocks_thrown += rocks_thrown_per_cycle * full_cycles_remaining
# so we don't get any more cache hit while we run through the last few
seen = {}
else:
seen[h] = (num_rocks_thrown, max_height)
num_rocks_thrown += 1
return get_num_rows(tube) + (height_increase_per_cycle * full_cycles_remaining)

View file

@ -4,6 +4,8 @@ Solutions to the [2022 Advent of Code](https://adventofcode.com/2022).
---
The red dotted line denotes 15 seconds.
![Running times](running-times.png)
---
@ -14,19 +16,20 @@ Solutions to the [2022 Advent of Code](https://adventofcode.com/2022).
| Day | Status | Solutions | Notes |
| ----------------------------------- | ------------------ | ---------- | ------ |
| 01 - Calorie Counting | ★ ★ | [Python](01-calorieCounting/py), [Nim](01-calorieCounting/nim), [Java](01-calorieCounting/java/src) | Summing numbers |
| 02 - Rock Paper Scissors | ★ ★ | [Python](02-rockPaperScissors/py), [Nim](02-rockPaperScissors/nim) | Programmatically playing Rock Paper Scissors |
| 03 - Rucksack Reorganization | ★ ★ | [Python](03-rucksackReorganization/py), [Nim](03-rucksackReorganization/nim) | Sets and intersections |
| 04 - Camp Cleanup | ★ ★ | [Python](04-campCleanup/py), [Nim](04-campCleanup/nim) | More sets and more intersections! |
| 05 - Supply Stacks | ★ ★ | [Python](05-supplyStacks/py), [Nim](05-supplyStacks/nim) | Believe it or not, this one involved stacks. |
| 06 - Tuning Trouble | ★ ★ | [Python](06-tuningTrouble/py), [Nim](06-tuningTrouble/nim) | This is the first year I've not repeatedly forgotten about the existence of sets, and it's coming in quite handy. |
| 07 - No Space Left On Device | ★ ★ | [Python](07-noSpaceLeftOnDevice/py) | Turns out that fake file systems are prone to very subtle and infuriating bugs. |
| 08 - Treetop Tree House | ★ ★ | [Python](08-treetopTreeHouse/py) | Magical coordinate dictionary tuple things do be magical. |
| 09 - Rope Bridge | ★ ★ | [Python](09-ropeBridge/py), [Nim](09-ropeBridge/nim) | Does this count as this year's first cellular automata? |
| 10 - Cathode-Ray Tube | ★ ★ | [Python](10-cathodeRayTube/py) | A nasty problem with a nasty solution and nasty outputs that mess with my framework. |
| 11 - Monkey in the Middle | ★ ★ | [Python](11-monkeyInTheMiddle/py) | Return of Advent of Maths! |
| 01 - Calorie Counting | ★ ★ | [Python](01-calorieCounting/py/__init__.py), [Nim](01-calorieCounting/nim/challenge.nim), [Java](01-calorieCounting/java/src) | Summing numbers |
| 02 - Rock Paper Scissors | ★ ★ | [Python](02-rockPaperScissors/py/__init__.py), [Nim](02-rockPaperScissors/nim/challenge.nim) | Programmatically playing Rock Paper Scissors |
| 03 - Rucksack Reorganization | ★ ★ | [Python](03-rucksackReorganization/py/__init__.py), [Nim](03-rucksackReorganization/nim/challenge.nim) | Sets and intersections |
| 04 - Camp Cleanup | ★ ★ | [Python](04-campCleanup/py/__init__.py), [Nim](04-campCleanup/nim/challenge.nim) | More sets and more intersections! |
| 05 - Supply Stacks | ★ ★ | [Python](05-supplyStacks/py/__init__.py), [Nim](05-supplyStacks/nim/challenge.nim) | Believe it or not, this one involved stacks. |
| 06 - Tuning Trouble | ★ ★ | [Python](06-tuningTrouble/py/__init__.py), [Nim](06-tuningTrouble/nim/challenge.nim) | This is the first year I've not repeatedly forgotten about the existence of sets, and it's coming in quite handy. |
| 07 - No Space Left On Device | ★ ★ | [Python](07-noSpaceLeftOnDevice/py/__init__.py) | Turns out that fake file systems are prone to very subtle and infuriating bugs. |
| 08 - Treetop Tree House | ★ ★ | [Python](08-treetopTreeHouse/py/__init__.py) | Magical coordinate dictionary tuple things do be magical. |
| 09 - Rope Bridge | ★ ★ | [Python](09-ropeBridge/py/__init__.py), [Nim](09-ropeBridge/nim/challenge.nim) | Does this count as this year's first cellular automata? |
| 10 - Cathode-Ray Tube | ★ ★ | [Python](10-cathodeRayTube/py/__init__.py) | A nasty problem with a nasty solution and nasty outputs that mess with my framework. |
| 11 - Monkey in the Middle | ★ ★ | [Python](11-monkeyInTheMiddle/py/__init__.py) | Return of Advent of Maths! |
| 12 - Hill Climbing Algorithm | ☆ ☆ | | |
| 13 - Distress Signal | ☆ ☆ | | |
| 14 - Regolith Reservoir | ★ ★ | [Python](14-regolithReservoir/py) | Simulating falling sand |
| 15 - Beacon Exclusion Zone | ★ ★ | [Python](15-beaconExclusionZone/py) | Searching through a 4000000^2 size grid for a literal single empty spot |
| 16 - Proboscidea Volcanium | ★ ★ | [Python](16-proboscideaVolcanium/py) | Nasty combinatorics |
| 14 - Regolith Reservoir | ★ ★ | [Python](14-regolithReservoir/py/__init__.py) | Simulating falling sand |
| 15 - Beacon Exclusion Zone | ★ ★ | [Python](15-beaconExclusionZone/py/__init__.py) | Searching through a 4000000^2 size grid for a literal single empty spot |
| 16 - Proboscidea Volcanium | ★ ★ | [Python](16-proboscideaVolcanium/py/__init__.py) | Nasty combinatorics |
| 17 - Pyroclastic Flow | ★ ★ | [Python](17-pyroclasticFlow/py/__init__.py) | Detecting cycles in a large amount of knock-off Tetris. |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Before After
Before After

View file

@ -58,6 +58,9 @@ figure = plt.figure(figsize=(25/2, 5))
axp1 = figure.add_subplot(1, 2, 1)
axp2 = figure.add_subplot(1, 2, 2, sharey=axp1)
axp1.axhline(y=15, color="#fc8080", linestyle="--")
axp2.axhline(y=15, color="#fc8080", linestyle="--")
for i, language in enumerate(benchmark_data):
data = benchmark_data[language]

View file

@ -1,5 +1,6 @@
from __future__ import annotations
from typing import Any, TypeVar, Callable, Iterable
from collections.abc import Sequence
class BaseChallenge:
@ -34,9 +35,16 @@ class Vector:
x: int
y: int
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __init__(self, *args):
if len(args) == 1 and Vector._is_vector_tuple(args[0]):
x, y = args[0]
elif len(args) != 2:
return ValueError("expected integer tuple or pair of integers")
else:
x, y = args
self.x = int(x)
self.y = int(y)
@staticmethod
def _is_vector_tuple(o: Any) -> bool:
@ -74,4 +82,30 @@ class Vector:
return f"({self.x}, {self.y})"
def __hash__(self):
return hash(f"{self.x},{self.y}")
return hash((self.x, self.y))
class Consumer:
x: Sequence[T]
i: int
def __init__(self, x: Sequence[T]):
self.x = x
self.i = 0
def take(self) -> T:
self.i += 1
return self.x[self.i-1]
def undo(self):
self.i -= 1
class RepeatingConsumer(Consumer):
def take(self) -> T:
val = super().take()
self.i = self.i % len(self.x)
return val
def undo(self):
super().undo()
if self.i < 0:
self.i += len(self.x)

View file

@ -121,6 +121,8 @@ func run() error {
} else {
fmt.Print("Running...\n\n")
if err := runTests(runner, challengeInfo); err != nil {
return err
}