Render individual aoc pages

This commit is contained in:
Maciej Jur 2023-04-08 02:32:54 +02:00
parent 76290df734
commit 2061c1e8b5
22 changed files with 1205 additions and 2 deletions

View file

@ -0,0 +1,36 @@
---
title: "Advent of Code 2021 - Day 1"
date: 2021-12-01T23:25:56+01:00
year: 2021
day: 1
stars: 2
---
First day of the Advent of Code turned out to be pretty easy, the first task was to find the number of consecutive increases in the sequence of numbers. The simplest way to do it would be to use a simple loop to compare each number with the one before it.
## Task 1
In my solution however I took a different approach - I sliced the sequence, so that there's two of them and they're offset by 1. I then used the `zip` function to zip the values together into tuples of the form `(previous, current)`. Then I used a generator to compare the values in tuple, which outputs True if `current` is greater than `previous` and False otherwise. Then I summed the resulting sequence of booleans... You can do that? In Python yes, because `True` is 1 and `False` is 0.
```py
def solve1() -> int:
with open('../.input/day01', 'r') as f:
numbers: list[int] = [int(line) for line in f.readlines()]
return sum((n > p) for p, n in zip(numbers[:-1], numbers[1:]))
```
## Task 2
In the second task I used the same approach, but this time I zipped 3 consecutive elements together to form a window tuple. Then I zipped another 3, but this time offset by 1. Then I zipped both of them together and used the `sum` function to sum the values of the tuples. The rest is analogous to the first task.
```py
def solve2() -> int:
with open('../.input/day01', 'r') as f:
numbers: list[int] = [int(line) for line in f.readlines()]
return sum((sum(n) > sum(p)) for p, n in zip(
zip(numbers[:-3], numbers[1:-2], numbers[2:-1]),
zip(numbers[1:-2], numbers[2:-1], numbers[3:])
))
```
Overall this task was pretty easy, but the second task took me a bit longer, I didn't really get what I was supposed to do at first :smile:

View file

@ -0,0 +1,83 @@
---
title: "Advent of Code 2021 - Day 2"
date: 2021-12-02T19:46:19+01:00
year: 2021
day: 2
stars: 2
---
Day 2 of Advent of Code is [here,](https://adventofcode.com/2021/day/2) and it turned out to be pretty easy. Starting off all we get is a list of instructions of the form `<instruction> <parameter>` where `<instruction>` is one of `forward`, `up` or `down` and `<parameter>` is an integer.
My idea to parse this list of instructions was to create a list of tuples of the form `(instruction, parameter)` using a regular expression `^([a-z]+) ([0-9]+)$`. This regular expression makes use of [capturing groups](https://docs.python.org/3/howto/regex.html#grouping) to extract the instruction and the parameter separately. The groups are accessed using `match.group(1)` and `match.group(2)`.
```python
pattern = re.compile('^([a-z]+) ([0-9]+)$')
def load_input() -> list[Tuple[str, int]]:
with open('../.input/day02', 'r') as f:
return [
(match.group(1), int(match.group(2))) for match
in (pattern.search(line.strip()) for line in f.readlines())
]
```
## Task 1
Here my idea was to use a dictionary to store functions (lambdas) under keys `'forward'`, `'up'` and `'down'`. The functions are then called with the parameter as argument together with the current position.
I used `functools.reduce` to fold the list of instructions into a single final position. Reduce represents in Python the mathematical concept of a [fold](https://en.wikipedia.org/wiki/Fold_(higher-order_function)), which also is a commonly used operation in functional programming. The function `functools.reduce` takes a function, a list and an initial value as arguments. The function is applied to the initial value and the first element of the list, then the result is used as the initial value for the next iteration. The result of the last iteration is returned.
This operation can be represented as `foldl f z xs` in some other functional languages:
```haskell
-- Haskell
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
```
Here's my solution:
```python
def solve1() -> int:
numbers = load_input()
instructions = {
"forward": lambda arg, x, y: (x + arg, y),
"up": lambda arg, x, y: (x, y + arg),
"down": lambda arg, x, y: (x, y - arg),
}
x, y = reduce(lambda pos, item: instructions[item[0]](item[1], *pos), numbers, (0, 0))
return abs(x) * abs(y)
```
The key part is right here:
```python
# unpacking the final position into x and y
x, y = reduce(
# Function to apply which takes the previous result (accumulator) and the next item in the list.
# We use dict to get the function from the dictionary.
# We use *pos to unpack accumulated value into function arguments.
lambda pos, item: instructions[item[0]](item[1], *pos),
# list of instructions to fold
numbers,
# initial value for the accumulator
(0, 0)
)
```
## Task 2
Task 2 was pretty much identical to task 1. I used a dictionary to store functions (lambdas) under keys `'forward'`, `'up'` and `'down'`, except that the functions have different bodies and they take an additional parameter `a` for aim.
```python
def solve2() -> int:
numbers = load_input()
instructions = {
"forward": lambda arg, x, y, a: (x + arg, y + a * arg, a),
"up": lambda arg, x, y, a: (x, y, a - arg),
"down": lambda arg, x, y, a: (x, y, a + arg),
}
x, y, _ = reduce(lambda pos, item: instructions[item[0]](item[1], *pos), numbers, (0, 0, 0))
return abs(x) * abs(y)
```

101
src/content/aoc/2021/03.md Normal file
View file

@ -0,0 +1,101 @@
---
title: "Advent of Code 2021 - Day 3"
date: 2021-12-03T16:51:41+01:00
year: 2021
day: 3
stars: 2
---
Today's challenge is [here](https://adventofcode.com/2021/day/3). It turned out to be pretty tricky with many possible approaches to solve it. First task was pretty trivial, but I kind of feel like my second solution was a bit overengineered :smile:
Loading data is pretty simple, all I had to do here was to load strings:
```python
def load_input() -> list[str]:
with open('../.input/day03', 'r') as f:
return [line.strip() for line in f.readlines()]
```
## Task 1
Here we had to find two binary numbers based on the frequency of digits at each position. I used a `dict` to store count of `0`s and `1`s at each position for all binary numbers. Then I used this `dict` to construct two binary numbers. The number for gamma consists of the most frequent digits at each position, and the number for epsilon consists of the least frequent digits at each position.
```python
def solve1() -> int:
input = load_input()
cache = [{"0": 0, "1": 0} for _ in range(len(input[0]))]
for binary in input:
for i, c in enumerate(binary):
cache[i][c] += 1
gamma, epsilon = map(sum, zip(*[
(2**i, 0) if c["1"] > c["0"] else (0, 2**i)
for i, c in enumerate(reversed(cache))
]))
return gamma * epsilon
```
After finding the binary numbers I converted both of them to decimal and multiplied them. An interesting part here is the usage of `zip(*iterable])` notation to turn a list of tuples into a tuple of lists.
It can be visualized as:
`[(x1, y1), (x2, y2), ...] -> [[x1, x2, ...], [y1, y2, ...]]`
## Task 2
Here we had to find a single remaining binary number for each case - oxygen and carbon. I used a `dict` once again to store the frequency of each digit at each position. Then I used this data to filter out the binary numbers until there is only one left. The part where I filter the numbers is also where I update the dict by subtracting the frequency, so that it stays up to date for the next iteration.
```python
def find_single(input: list[str], reverse: bool = False) -> int:
cache = [{"0": 0, "1": 0} for _ in range(len(input[0]))]
remaining = input[:]
for binary in remaining:
for i, digit in enumerate(binary):
cache[i][digit] += 1
for i, c in enumerate(cache):
if len(remaining) == 1:
break
generator = (entry for entry in remaining)
remaining = []
winning_digit = ("0" if c["0"] <= c["1"] else "1") if reverse else ("1" if c["1"] >= c["0"] else "0")
for binary in generator:
if binary[i] == winning_digit:
remaining.append(binary)
else:
for digit, cache_dict in zip(binary, cache):
cache_dict[digit] -= 1
return remaining[0]
```
Here I use the function to find the multiply from the resulting numbers for oxygen and cargon:
```python
def solve2() -> int:
input = load_input()
oxygen, carbon = input[:], input[:]
res_o = sum(2**int(i) for i, x in enumerate(reversed(find_single(oxygen, reverse=False))) if x == "1")
res_c = sum(2**int(i) for i, x in enumerate(reversed(find_single(carbon, reverse=True))) if x == "1")
return res_o * res_c
```
## Bonus: Task 1 in NumPy
The way this works is it loads all data into a single matrix, calculates mean value for each place, converts to nearest integer. The other thing is the `logical_not` of that. Then you get decimal number by calculating dot product of the binary number and a `[1, 2, 4, 8, ...]` vector reversed.
```python
import numpy as np
def load_input_np() -> np.ndarray:
with open('../.input/day03', 'r') as f:
return np.array([list(map(np.int32, list(i.strip()))) for i in f.readlines()])
def solve1_np() -> int:
input = load_input_np()
binary = input.mean(axis=0) >= 0.5
invert = ~binary
decimalize = 2 ** np.arange(binary.shape[0])[::-1]
return sum(binary * decimalize) * sum(invert * decimalize)
```

View file

@ -0,0 +1,58 @@
---
title: "Advent of Code 2021 - Day 4"
date: 2021-12-04T11:23:18+01:00
year: 2021
day: 4
stars: 2
math: true
---
Today's problem is [Day 4](https://adventofcode.com/2021/day/4). This time the challenge is inspied by the game called Bingo. We get a vector of number and a number of bingo boards. We need to find the board that wins first and last, then we need to calculate score for it.
Loading data from file into NumPy arrays:
```python
def loader_np() -> np.ndarray:
with open('../.input/day04', 'r') as f:
data = f.read().splitlines()
vector = np.array([int(x) for x in data[0].split(",")])
tensor = np.array([
np.array(list(map(np.int32, map(str.split, board))))
for board
in [data[i:i+5] for i in range(2, len(data), 6)]
])
return vector, tensor
```
## Task 1
Here we need to find the first winning board. I used the `np.isin` function to find the mask for all board, for example given vector `[1, 2, 3, 4, 5]` and matrix `[[1, 2, 3], [4, 5, 6]]` we get `[[True, True, True], [True, True, False]]`. The vector changes size through slicing, with each iteration of the outer loop - it simulates drawing a new number. The inner for loop iterates through all boards (matrices), there we check if on any axis there's a full row or column of matching numbers. If there's a match we calculate the score.
```python
def solve1() -> int:
vector, tensor = loader_np()
for scale in range(1, len(vector)+1):
for matrix in tensor:
mask = np.isin(matrix, vector[:scale])
if mask.all(axis=0).any() or mask.all(axis=1).any():
return (matrix * ~mask).sum() * vector[scale-1]
```
The score is calculated by taking a dot product of the matrix with the negation of the mask (which eliminates unmarked numbers), summing the remaining values and multiplying by the value of the drawn number.
$$ \text{score} = v_{drawn} \cdot \sum_{i=1}^{n} \sum_{j=1}^{m} \text{board}[i,j] \cdot \neg \text{mask}[i,j] $$
## Task 2
This is analogous to the first task, but we need to find the last winning board. I iterate from the end of the vector. The inner loop is similar to the one in the first task, just needed to find when the first board doesn't match and then calculate another mask for the round afterwards (scale+1).
```python
def solve2() -> int:
vector, tensor = loader_np()
for scale in range(len(vector)+1, 0, -1):
for matrix in tensor:
mask = np.isin(matrix, vector[:scale])
if not mask.all(axis=0).any() and not mask.all(axis=1).any():
prev_mask = np.isin(matrix, vector[:scale+1])
return (matrix * ~prev_mask).sum() * vector[scale]
```

View file

@ -0,0 +1,70 @@
---
title: "Advent of Code 2021 - Day 5"
date: 2021-12-05T13:51:01+01:00
year: 2021
day: 5
stars: 2
---
Today's challenge is [here](https://adventofcode.com/2021/day/5). Here we get a punch of lines in the form of two points and we have to find number of points where they cross. For parsing the data I used a regular expression `(\d+),(\d+) -> (\d+),(\d+)`.
Loading data from the input file:
```python
pattern = re.compile(r'(\d+),(\d+) -> (\d+),(\d+)')
def loader() -> list[Tuple[Tuple[int, int], Tuple[int, int]]]:
with open('../.input/day05', 'r') as f:
return [
((int(x1), int(y1)), (int(x2), int(y2)))
for x1, y1, x2, y2
in pattern.findall(f.read())
]
```
## Task 1
In this task we have to find number of points where lines cross, but we have to skip the diagonal lines. When I was solving this task I did a mistake and I included the logic for counting the diagonal lines as well.
Here is the function for generating the list of points for any line. I used generators pretty liberally as well as the `itertools.repeat` function, which is used to generate infinite sequences of a single value. I had to use that for cases where the line is vertical or horizontal, because otherwise the zip function would stop at the first iteration.
```python
def find_points_in_line(x1, y1, x2, y2, skip_diagonal: bool = False) -> Iterable[Tuple[int, int]]:
if skip_diagonal and x1 != x2 and y1 != y2:
return []
if x2 < x1 and y2 < y1:
return find_points_in_line(x2, y2, x1, y1)
if x2 < x1:
y = (y for y in range(y1, y2 + 1)) if y1 != y2 else repeat(y1)
return zip(range(x1, x2 - 1, -1), y)
if y2 < y1:
x = (x for x in range(x1, x2 + 1)) if x1 != x2 else repeat(x1)
return zip(x, range(y1, y2 - 1, -1))
if x1 == x2:
return zip(repeat(x1), range(min(y1, y2), max(y1, y2) + 1))
if y1 == y2:
return zip(range(min(x1, x2), max(x1, x2) + 1), repeat(y1))
return zip(range(x1, x2 + 1), range(y1, y2 + 1))
```
Here we use two sets to store the visited points as well as the points where lines cross. We can do this because we don't need to know how many lines cross at any given point, just the number of crossing points in general. A set is a perfect choice for this, it saves us memory and time.
```python
def solve1(skip_diagonal=True) -> int:
visited_pts = set()
crossed_pts = set()
for (start, end) in loader():
for x, y in find_points_in_line(*start, *end, skip_diagonal):
if (x, y) in visited_pts:
crossed_pts.add((x, y))
else:
visited_pts.add((x, y))
return len(crossed_pts)
```
## Task 2
Do I need to say anything here? :smile:
```python
def solve2() -> int:
return solve1(skip_diagonal=False)
```

View file

@ -0,0 +1,72 @@
---
title: "Advent of Code 2021 - Day 6"
date: 2021-12-06T19:40:20+01:00
year: 2021
day: 6
stars: 2
---
Today's challenge is [here](https://adventofcode.com/2021/day/6). We have to find the amount of fish 'split' after some days. A problem based on [exponential growth](https://en.wikipedia.org/wiki/Exponential_growth).
When I started doing this challenge I first thought about using Object-Oriented Programming, but that didn't work out. When calculating large amounts of fish I started to run out of memory and the algorithm was too slow.
Loading the data:
```python
def load() -> list[int]:
with open('../.input/day06') as f:
return [int(line) for line in f.read().split(",")]
```
## Task 1
Pretty simple, we just need to advance the fish and then count the amount of fish. I created simple methods in the `LanternFish` class that do just that.
Here's a class that represents a fish:
```python
class LanternFish:
def __init__(self, cycle: int, current: int = None):
self.cycle = cycle
self.current = current or cycle
self.children = []
def advance(self) -> None:
self.current -= 1
[child.advance() for child in self.children]
if self.current == -1:
self.current = self.cycle
self.children.append(LanternFish(cycle=6, current=8))
def count(self) -> int:
return 1 + sum([child.count() for child in self.children])
```
Here we iterate through number of days and advance the fish. The way fishes are related to each other is that they all form a tree. When I advance a fish, I also advance all of its children. The amount of fish is the sum of the amount of fish in the tree.
```python
def solve1() -> int:
fishes = [LanternFish(cycle=6, current=init) for init in load()]
[[fish.advance() for fish in fishes] for _ in range(80)]
return sum([fish.count() for fish in fishes])
```
## Task 2
Here I took a different approach, I stored the fishes in a simple dictionary. The key is the day and the value is the number of fishes. I then iterate through the days and advance the fishes.
The way this works is kind of hard to explain really, but the key to it all is the `%` [modulo operator](https://en.wikipedia.org/wiki/Modulo_operation). The modulo operator returns the remainder of a division. In this case, the modulo operator is used to determine whether some fish should give birth.
```python
def solve2() -> int:
fishes = { i: 0 for i in range(7) }
newborns = { i: 0 for i in range(9) }
for init in load():
fishes[init] += 1
for epoch in range(256):
born_from_old = fishes[epoch % 7]
fishes[epoch % 7] += newborns[epoch % 9]
newborns[epoch % 9] += born_from_old
return sum(fishes.values()) + sum(newborns.values())
```

View file

@ -0,0 +1,73 @@
---
title: "Advent of Code 2021 - Day 7"
date: 2021-12-07T22:07:00+01:00
year: 2021
math: true
day: 7
stars: 2
---
Today's challenge is [here](https://adventofcode.com/2021/day/7). We have to find the optimal point for all crabs to move to on a single axis. The optimal point is the point where the fuel expenditure to all other points is minimal.
Loading the input data:
```python
def load() -> list[int]:
with open('../.input/day07') as f:
return [int(x) for x in f.readline().split(',')]
```
## Task 1
In this task the way to calculate the fuel expenditure is to sum up the fuel expenditure to all other points. The way it's calculated is linear. Formula for fuel expenditure, where C is the set of all points and x is the point we want to calculate the fuel expenditure for:
$$\sum_{c \in C} |x - c|$$
We are looking for the point with minimal fuel expenditure:
$$\min_{x} \sum_{c \in C} |x - c|$$
The way to find the optimal point is to find the median of all the points where crabs are placed, because the median happens to be the point where the fuel expenditure is minimal. Formula for the median:
$$\frac{1}{2} \sum_{c \in C} c$$
I don't really know how this works myself, but the proof for why it works can be found [here](https://math.stackexchange.com/questions/113270/the-median-minimizes-the-sum-of-absolute-deviations-the-ell-1-norm).
Here's the code for the task:
```python
def solve1() -> int:
numbers = load()
median = sorted(numbers)[len(numbers) // 2]
return sum(abs(x - median) for x in numbers)
```
## Task 2
In this task we have to find the optimal point for all crabs to move to on a single axis, similar to task 1. The difference is that the way fuel expenditure is caluculated is a sequence: 1, 1+2, 1+2+3, 1+2+3+4, ...
The way I solved this is I looked up the formula for the function that calculates the value for the n-th element in the sequence on the Internet. It happens to be:
$$ f(n) = \frac{n * (n + 1)}{2} $$
I implemented this formula in the code below:
```python
def nth_sum(n: int) -> int:
return n * (n + 1) // 2
```
In the solution to the task 2 I used NumPy to create a matrix of the form:
$$ \begin{bmatrix} 0 & 0 & 0 & \cdots & 0 \\\\ 1 & 1 & 1 & \cdots & 1 \\\\ 2 & 2 & 2 & \cdots & 2 \\\\ \vdots & \vdots & \vdots & \ddots & \vdots \\\\ n & n & n & \cdots & n \end{bmatrix} $$
Then I subtracted the vector of the crab positions from the matrix. The result is a matrix with the distances from the crab to all other possible points.
$$ \begin{bmatrix} |0 - c_1| & |0 - c_2| & |0 - c_3| & \cdots & |0 - c_i| \\\\ |1 - c_1| & |1 - c_2| & |1 - c_3| & \cdots & |1 - c_i| \\\\ |2 - c_1| & |2 - c_2| & |2 - c_3| & \cdots & |2 - c_i| \\\\ \vdots & \vdots & \vdots & \ddots & \vdots \\\\ |n - c_1| & |n - c_2| & |n - c_3| & \cdots & |n - c_i| \end{bmatrix} $$
We then apply the formula for the n-th element in the sequence to the matrix. The result is a matrix with the fuel expenditure to all other points. When we sum the values in rows of the matrix we get the total fuel expenditure to all points for the crabs.
$$ \min \sum_{c \in C} f(|x - c|) $$
By finding the row with the minimal fuel expenditure we can find the optimal point. Below is the implementation in NumPy:
```python
def solve2() -> int:
numbers = np.array(load())
search_vector = np.arange(0, max(numbers) + 1)
search_matrix = np.tile(search_vector, (len(numbers), 1)).T
distance = abs(search_matrix - numbers)
fuel_expended = nth_sum(distance)
return min(fuel_expended.sum(axis=1))
```

122
src/content/aoc/2021/08.md Normal file
View file

@ -0,0 +1,122 @@
---
title: "Advent of Code 2021 - Day 8"
date: 2021-12-14T23:42:27+01:00
year: 2021
day: 8
stars: 2
---
```python
from functools import reduce
def load() -> list[tuple[list[str], ...]]:
with open('../.input/day08', 'r') as f:
return [tuple(map(str.split, line.split('|'))) for line in f.read().splitlines()]
def solve1() -> int:
return sum(
len(list(filter(lambda x: len(x) in [2, 3, 4, 7], output)))
for _, output in load()
)
A = 0b0000001
B = 0b0000010
C = 0b0000100
D = 0b0001000
E = 0b0010000
F = 0b0100000
G = 0b1000000
n_to_s = {
0: A | B | C | E | F | G,
1: C | F,
2: A | C | D | E | G,
3: A | C | D | F | G,
4: B | C | D | F,
5: A | B | D | F | G,
6: A | B | D | E | F | G,
7: A | C | F,
8: A | B | C | D | E | F | G,
9: A | B | C | D | F | G,
}
s_to_n = {
A | B | C | E | F | G: 0,
C | F: 1,
A | C | D | E | G: 2,
A | C | D | F | G: 3,
B | C | D | F: 4,
A | B | D | F | G: 5,
A | B | D | E | F | G: 6,
A | C | F: 7,
A | B | C | D | E | F | G: 8,
A | B | C | D | F | G: 9,
}
possible = {
2: n_to_s[1],
3: n_to_s[7],
4: n_to_s[4],
}
def negate(string: str):
return list(filter(lambda x: x not in string, "abcdefg"))
def check_pattern(pattern: list[str], mapping: dict[str, int]):
def check_legal(_str: str):
return reduce(lambda a, c: a | mapping[c], _str, 0) in s_to_n
return all(map(check_legal, pattern))
def find_mapping(pattern: list[str]):
domains = {
char: 0b1111111
for char in 'abcdefg'
}
# remove obvious impossible choices
for activation in pattern:
length = len(activation)
if length in possible:
for char in activation:
domains[char] &= possible[length]
for char in negate(activation):
domains[char] &= ~possible[length]
# bruteforce search
search: list[dict[str, int]] = [{}]
for char, domain in domains.items():
deeper = []
for i in range(7):
if domain & (1 << i):
for s in search:
new = s.copy()
new[char] = (1 << i)
deeper.append(new)
search = deeper
for mapping in search:
if check_pattern(pattern, mapping):
return mapping
def string_to_int(string: str, mapping: dict) -> int:
return s_to_n[reduce(lambda a, c: a | mapping[c], string, 0)]
def solve2() -> int:
return sum(
reduce(lambda a, c: a * 10 + string_to_int(c, find_mapping(pattern)), output, 0)
for pattern, output in load()
)
if __name__ == '__main__':
print(solve1()) # 301
print(solve2()) # 908067
```

View file

@ -0,0 +1,62 @@
---
title: "Advent of Code 2021 - Day 9"
date: 2021-12-14T23:42:35+01:00
year: 2021
day: 9
stars: 2
---
```python
from functools import reduce
import numpy as np
from numpy.lib.stride_tricks import as_strided
def load() -> np.ndarray:
with open("../.input/day09") as f:
lines = list(map(str.strip, f.readlines()))
return np.array([int(num) for line in lines for num in line]).reshape((len(lines), -1))
def windows(target, shape=(3, 3), stride: int = 1):
target = np.pad(target, 1, 'constant', constant_values=9)
(t_h, t_w), (w_h, w_w) = target.shape, shape
out_shape = (1 + (t_h - w_h) // stride, 1 + (t_w - w_w) // stride, w_h, w_w)
out_strides = (target.strides[0] * stride, target.strides[1] * stride, target.strides[0], target.strides[1])
return as_strided(target, shape=out_shape, strides=out_strides)
def solve1() -> int:
hmap = load()
return np.sum((hmap + 1) * (hmap == windows(hmap).min(axis=(2, 3))))
def find_basin(areas, visited, x, y) -> int:
if visited[x, y]:
return 0
visited[x, y] = True
area = 1
for dx, dy in [(x+1, y), (x-1, y), (x, y+1), (x, y-1)]:
if (
0 <= dx < areas.shape[0] and
0 <= dy < areas.shape[1] and
areas[dx, dy]
):
area += find_basin(areas, visited, dx, dy)
return area
def solve2() -> int:
hmap = load()
areas = hmap != 9
starts = np.argwhere(hmap == windows(hmap).min(axis=(2, 3)))
visited = np.zeros(hmap.shape, dtype=np.bool8)
basins = [find_basin(areas, visited, *start) for start in starts]
return reduce(lambda acc, num: acc * num, sorted(basins, reverse=True)[:3], 1)
if __name__ == "__main__":
print(solve1()) # 522
print(solve2()) # 916688
```

View file

@ -0,0 +1,62 @@
---
title: "Advent of Code 2021 - Day 10"
date: 2021-12-14T23:42:39+01:00
year: 2021
day: 10
stars: 2
---
```python
from functools import reduce
def load() -> list[str]:
with open('../.input/day10', 'r') as f:
return list(map(str.strip, f.readlines()))
opening, closing = ['(', '[', '{', '<'], [')', ']', '}', '>']
scores = { ")": 3, "]": 57, "}": 1197, ">": 25137 }
def validate(line: str) -> int:
stack = []
for char in line:
if char in opening:
stack.append(char)
else:
if opening.index(stack.pop()) != closing.index(char):
return scores[char]
return 0
def solve1() -> int:
return sum(map(validate, load()))
scores2 = { ")": 1, "]": 2, "}": 3, ">": 4 }
completion = { o: c for o, c in zip(opening, closing) }
def autocomplete(line: str) -> str:
stack = []
for char in line:
if char in opening:
stack.append(char)
else:
stack.pop()
return list(map(lambda x: completion[x], stack))
def solve2() -> int:
lines = list(filter(lambda line: validate(line) == 0, load()))
return sorted([
reduce(lambda acc, char: 5 * acc + scores2[char], reversed(line), 0)
for line in map(autocomplete, lines)
])[len(lines) // 2]
if __name__ == '__main__':
print(solve1()) # 345441
print(solve2()) # 3235371166
```

View file

@ -0,0 +1,90 @@
---
title: "Advent of Code 2021 - Day 11"
date: 2021-12-11T09:09:14+01:00
year: 2021
math: true
day: 11
stars: 2
---
Today's challenge is available [here](https://adventofcode.com/2021/day/11).
Octopuses flash when their energy levels reach above 9 and it causes their neignbours to increase their levels by 1 too? This sounds like a perfect job for some kind of a matrix operation...
Loading data:
```py
def load():
with open('../.input/day11') as f:
lines = list(map(str.strip, f.readlines()))
return np.array([int(num) for line in lines for num in line]).reshape((-1, len(lines)))
```
In this chanllenge I decided to try my hand at using convolution matrix to solve this problem. First I had to create a function that would return an array containing octopuses and their neighbours. For this I used the following code:
```py
from numpy.lib.stride_tricks import as_strided
def windows(target, shape=(3, 3), stride: int = 1):
target = np.pad(target, 1, 'constant', constant_values=0)
(t_h, t_w), (w_h, w_w) = target.shape, shape
out_shape = (
1 + (t_h - w_h) // stride,
1 + (t_w - w_w) // stride,
w_h, w_w
)
out_strides = (
target.strides[0] * stride,
target.strides[1] * stride,
target.strides[0], target.strides[1]
)
return as_strided(target, shape=out_shape, strides=out_strides)
```
The function `as_strided` is a pretty esoteric function, which operates directly on memory, which also means a lot can go very wrong when using it.
Here I used this function to calculate how the energy levels would progress through the time:
```py
def step(input: np.ndarray):
current = input + np.ones(input.shape, dtype=np.int32)
flashed = np.zeros(input.shape, dtype=np.bool8)
while np.any(now_flashed := ((current * ~flashed) > 9)):
convolution = np.tensordot(
windows(now_flashed.astype(np.int32)),
np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]]),
axes=((2, 3), (0, 1))
)
flashed |= now_flashed
current = current + convolution * ~flashed
return np.where(current > 9, 0, current), flashed
```
I used a convolution matrix of the following shape:
$$ \begin{bmatrix} 1 & 1 & 1 \\\\ 1 & 0 & 1 \\\\ 1 & 1 & 1 \end{bmatrix} $$
## Task 1
Here we sum all the flashes that happened in all of the iterations with `np.sum`:
```py
def solve1() -> int:
input = load()
flash_count = 0
for _ in range(100):
input, flashed = step(input)
flash_count += np.sum(flashed)
return flash_count
```
## Task 2
Here we check when all the octopuses flashed with `np.all`:
```py
def solve2() -> int:
input = load()
steps = 0
while (steps := steps + 1):
input, flashed = step(input)
if np.all(flashed):
return steps
```

View file

@ -0,0 +1,58 @@
---
title: "Advent of Code 2021 - Day 12"
date: 2021-12-14T23:42:43+01:00
year: 2021
day: 12
stars: 2
---
```python
import re
input_pattern = re.compile(r'([a-zA-Z]+)-([a-zA-Z]+)')
small_pattern = re.compile(r'([a-z]+)')
def load():
with open('../.input/day12', 'r') as f:
return input_pattern.findall(f.read())
def construct_paths(graph, start, end, path=None, single_reentry=False):
if path is None:
path = [start]
if start == end:
yield path
else:
for node in graph[start]:
is_small = small_pattern.match(node)
if not is_small or node not in path:
yield from construct_paths(graph, node, end, path + [node], single_reentry)
elif is_small and single_reentry and node not in ["start", "end"]:
yield from construct_paths(graph, node, end, path + [node])
def solve1():
graph = {}
for node1, node2 in load():
graph.setdefault(node1, set()).add(node2)
graph.setdefault(node2, set()).add(node1)
return sum(1 for _ in construct_paths(graph, 'start', 'end'))
def solve2():
graph = {}
for node1, node2 in load():
graph.setdefault(node1, set()).add(node2)
graph.setdefault(node2, set()).add(node1)
return sum(1 for _ in construct_paths(graph, 'start', 'end', single_reentry=True))
if __name__ == '__main__':
print(solve1()) # 4304
print(solve2()) # 118242
```

View file

@ -0,0 +1,62 @@
---
title: "Advent of Code 2021 - Day 13"
date: 2021-12-14T23:42:45+01:00
year: 2021
day: 13
stars: 2
---
```python
import re
import numpy as np
pattern_dots = re.compile(r"([0-9]+),([0-9]+)")
pattern_folds = re.compile(r"fold along ([xy])=([0-9]+)")
def load():
with open("../.input/day13") as f:
dots, folds = f.read().split("\n\n")
dots = [(int(x), int(y)) for x, y in pattern_dots.findall(dots)]
matrix = np.zeros((1 + max(y for _, y in dots), 1 + max(x for x, _ in dots)), dtype=bool)
for x, y in dots:
matrix[y, x] = True
return matrix, [(axis, int(offset)) for axis, offset in pattern_folds.findall(folds)]
def fold(matrix: np.ndarray, axis, offset) -> np.ndarray:
if axis == "x":
result = matrix[:, :offset]
right = np.fliplr(matrix[:, offset+1:])
if result.shape == right.shape:
result |= right
else:
result[:, -right.shape[1]:] |= right
else:
result = matrix[:offset, :]
bottom = np.flipud(matrix[offset+1:, :])
if result.shape == bottom.shape:
result |= bottom
else:
result[-bottom.shape[0]:, :] |= bottom
return result
def solve1() -> int:
dots, folds = load()
return fold(dots, *folds[0]).sum()
def solve2() -> None:
dots, folds = load()
for axis, offset in folds:
dots = fold(dots, axis, offset)
[print("".join(line)) for line in np.where(dots, "#", " ")]
if __name__ == "__main__":
print(solve1()) # 638
solve2() # CJCKEBAPB
```

View file

@ -0,0 +1,81 @@
---
title: "Advent of Code 2021 - Day 14"
date: 2021-12-14T23:42:47+01:00
year: 2021
day: 14
stars: 2
---
```python
import re
input_pattern = re.compile(r"([A-Z]+) -> ([A-Z]+)")
def load():
with open("../.input/day14", "r") as f:
start, rest = f.read().split("\n\n")
return start, input_pattern.findall(rest)
def step(start, rules):
output = []
for pair in zip(start, start[1:]):
c1, c2 = pair
output.append(c1)
output.append(rules[c1 + c2])
output.append(start[-1])
return output
def find_counts(start: str, rules: dict[str, str], epochs: int) -> dict[str, int]:
for _ in range(epochs):
start = step(start, rules)
chars: dict[str, int] = {}
for c in start:
chars[c] = chars.get(c, 0) + 1
return chars
def solve1() -> int:
start, rules = load()
rules = {k: v for k, v in rules}
chars = find_counts(start, rules, 10)
values = list(sorted(chars.values(), reverse=True))
return values[0] - values[-1]
def find_counts_optimized(start: str, rules: dict[str, str], epochs: int) -> dict[str, int]:
chars: dict[str, int] = {}
for c1, c2 in zip(start, start[1:]):
chars[c1 + c2] = chars.get(c1 + c2, 0) + 1
for _ in range(epochs):
new_chars: dict[str, int] = {}
for combo, count in chars.items():
if combo in rules:
insert = rules[combo]
new_chars[combo[0] + insert] = new_chars.get(combo[0] + insert, 0) + chars[combo]
new_chars[insert + combo[1]] = new_chars.get(insert + combo[1], 0) + chars[combo]
chars = new_chars
final_chars: dict[str, int] = {start[-1]: 1}
for combo, count in chars.items():
final_chars[combo[0]] = final_chars.get(combo[0], 0) + count
return final_chars
def solve2():
start, rules = load()
rules = {k: v for k, v in rules}
chars = find_counts_optimized(start, rules, 40)
values = list(sorted(chars.values(), reverse=True))
return values[0] - values[-1]
if __name__ == "__main__":
print(solve1())
print(solve2())
```

View file

@ -0,0 +1,82 @@
---
title: "Advent of Code 2021 - Day 15"
date: 2021-12-17T00:28:20+01:00
year: 2021
day: 15
stars: 2
---
```python
import queue
import numpy as np
def load() -> np.ndarray:
with open('../.input/day15') as f:
lines = list(map(str.strip, f.readlines()))
return np.array([int(num) for line in lines for num in line]).reshape((-1, len(lines)))
def manhattan(node, goal) -> int:
return abs(node[0] - goal[0]) + abs(node[1] - goal[1])
def neighbors(node, shape) -> list[tuple[int, int]]:
return list(filter(
lambda t: 0 <= t[0] < shape[0] and 0 <= t[1] < shape[1],
[(node[0] + 1, node[1]), (node[0] - 1, node[1]), (node[0], node[1] + 1), (node[0], node[1] - 1)]
))
def a_star(
start: tuple[int, int],
goal: tuple[int, int],
grid: np.ndarray,
) -> list[tuple[int, int]]:
frontier: queue.Queue[tuple[int, tuple[int, int]]] = queue.PriorityQueue()
parent: dict[tuple[int, int], tuple[int, int]] = {}
cost: dict[tuple[int, int], int] = {start: 0}
frontier.put((0, start))
while not frontier.empty():
current: tuple[int, int] = frontier.get()[1]
if current == goal:
break
for neighbor in neighbors(current, grid.shape):
new_cost: int = cost[current] + grid[neighbor[1]][neighbor[0]]
if neighbor not in cost or new_cost < cost[neighbor]:
cost[neighbor], parent[neighbor] = new_cost, current
frontier.put((
new_cost + manhattan(neighbor, goal),
neighbor
))
path: list[tuple[int, int]] = [goal]
while path[-1] != start:
path.append(parent[path[-1]])
return path[::-1]
def solve1() -> int:
grid = load()
path = a_star((0, 0), (len(grid[0]) - 1, len(grid) - 1), grid)
return sum(grid[y][x] for x, y in path[1:])
def solve2() -> int:
full_grid = np.tile((grid := load()), (5, 5))
s_x, s_y = grid.shape[0], grid.shape[1]
for i in range(5):
for j in range(5):
full_grid[i*s_y:i*s_y+s_y, j*s_x:j*s_x+s_x] += np.full_like(grid, i+j)
full_grid = full_grid % 10 + (full_grid >= 10).astype(int)
path = a_star((0, 0), (full_grid.shape[0] - 1, full_grid.shape[1] - 1), full_grid)
return sum(full_grid[y][x] for x, y in path[1:])
if __name__ == "__main__":
print(solve1()) # 748
print(solve2()) # 3045
```

View file

@ -0,0 +1,31 @@
---
title: "Advent of Code 2021 - Day 16"
date: 2021-12-18T01:44:09+01:00
year: 2021
day: 16
stars: 0
---
Today's challenge is [here](https://adventofcode.com/2021/day/16).
My first attempt at solving this puzzle was to use a library called [construct](https://pypi.org/project/construct/) to parse the data, however it turned out to be too hard for me xD
I managed to create a parser for the literals:
```python
literal = Bitwise(Aligned(8, Struct(
"version" / BitsInteger(3),
"type" / Const(0b100, BitsInteger(3)),
"sequence" / RepeatUntil(
lambda x, _, __: x.flag == 0,
Struct(
"flag" / Flag,
"data" / Nibble,
),
),
"value" / Computed(lambda ctx: reduce(lambda acc, n: (acc << 4) + n.data, ctx.sequence, 0)),
)))
assert(literal.parse(b"\xD2\xFE\x28").value == 2021)
```
This does work as excepted, but given that the entire problem is a tree of structures, I might just write a bunch of functions to parse the data instead. In construct the recursion has to be done by using `LazyBound` as far as I can tell, which doesn't really work with `Bitwise`.

15
src/content/config.ts Normal file
View file

@ -0,0 +1,15 @@
import { defineCollection, z } from 'astro:content';
export const collections = {
aoc: defineCollection({
schema: z.object({
title: z.string(),
date: z.date(),
year: z.number(),
day: z.number(),
stars: z.number(),
math: z.boolean().optional()
})
}),
}

1
src/env.d.ts vendored
View file

@ -1 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

View file

@ -5,7 +5,7 @@ import Header from "../components/headers/Article.astro";
import Toc from "../components/Toc.astro";
interface Props {
headings: any[],
headings?: any[],
frontmatter: {
title: string;
date: string;
@ -25,7 +25,7 @@ const {
<main class="l-article">
<aside class="l-article__aside">
<div class="l-article__toc">
<Toc headings={headings} />
{headings && (<Toc headings={headings} />)}
</div>
</aside>
<article class="l-article__article marker readable">

View file

@ -0,0 +1,16 @@
---
import Article from '../../layouts/Article.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths () {
return (await getCollection('aoc'))
.map(entry => ({params: {slug: entry.slug}, props: {entry}}));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<Article frontmatter={entry.data}>
<Content />
</Article>

View file

@ -0,0 +1,18 @@
---
import { getCollection } from 'astro:content';
export async function getStaticPaths () {
return (await getCollection('aoc'))
.map(entry => entry.slug.slice(0, 4))
.map(year => ({params: {year}, props: {year}}))
}
const { year } = Astro.props;
const pages = await getCollection('aoc', (entry) => entry.id.startsWith(Astro.params.year!));
---
{pages.map(entry => (
<div>
{entry.id}
</div>
))}

10
src/pages/aoc/index.astro Normal file
View file

@ -0,0 +1,10 @@
---
import { getCollection } from 'astro:content';
const posts = await getCollection('aoc');
---
<div>{posts.length}</div>