This commit is contained in:
2024-12-08 21:36:04 +01:00
parent 9ef0275a61
commit 0ba1f371ed
18 changed files with 1016 additions and 115 deletions

45
2024/01/main.py Normal file
View File

@@ -0,0 +1,45 @@
from typing import List
def calc(raw: str) -> int:
left: List[int] = []
right: List[int] = []
for line in raw.splitlines():
[left_raw, right_raw] = line.split(" ")
left.append(int(left_raw))
right.append(int(right_raw))
left.sort()
right.sort()
# Part 1
part1 = 0
for i in range(len(left)):
diff = abs(left[i] - right[i])
part1 += diff
# Part 2
part2 = 0
counts = {}
for x in right:
if x in counts:
counts[x] += 1
else:
counts[x] = 1
for x in left:
if x in counts:
part2 += x * counts[x]
return (part1, part2)
# Test
with open("./2024/01/test.txt", "r") as f:
result = calc(f.read().strip())
print(result)
# Input
with open("./2024/01/input.txt", "r") as f:
result = calc(f.read().strip())
print(result)

26
2024/02/findings.md Normal file
View File

@@ -0,0 +1,26 @@
# Day 2
In part two i got stuck because I wanted to be smart. Don't be smart, unless you really are, unlike me.
So [the challenge](https://adventofcode.com/2024/day/2) was to allow one possible error in a _report_
Given the level `1 3 2 4 5`, which fails because the 2 decreases, instead of increasing, i though it would be neat to only try removing one of each numbers.
In this case either the `3` or the `2`. I did not want to brute force through the array. What was i missing?
I then quickly found out, as the brute force variant is trivial to implement (on error, just retry every combination with one missing, breaking at the first _safe_ combination). The output was correct with the brute force variant, but my "smart™" solution was off by very little, so what were the few edge cases?
These 4 are safe with the _Problem Dampener_, but were not detected by my "smart™" algorithm.
```
[55, 52, 53, 54, 56, 57]
[17, 15, 16, 19, 20, 23, 25]
[56, 57, 56, 55, 54, 51, 50, 49]
[81, 84, 81, 80, 77, 75, 72, 69]
```
Why? well the issue was, that the error always occurred when the wrong number was the first one, giving a bad prediction for the "direction", basically whether the numbers would increase or decrease. Since the issue was always the third number with index `2`, removing either the second or third element was not enough, as the root cause was that the first items was already wrong, but only detected to late.
## Possible solutions
In my head, another way to visualise it, was to think in differences, where the first element would be always 0, and the next ones the diff to the previous one. In that case we could check for steady increase/decrease by making sure that all the subsequent numbers are all positive or negative.
Basically like a drawn line, with one outlier.

80
2024/02/main.py Normal file
View File

@@ -0,0 +1,80 @@
def is_safe_1(numbers, tolerance=0) -> bool:
if len(numbers) < 2:
return True
down = numbers[0] > numbers[1]
cur = numbers[0]
for i in range(1, len(numbers)):
x = numbers[i]
diff = x - cur
if down:
diff *= -1
if 3 < diff or diff < 1:
if tolerance == 0:
return False
else:
found = set()
for j in range(len(numbers)):
tmp = numbers[:]
del tmp[j]
if is_safe_1(tmp, tolerance - 1):
found.add(tuple(tmp))
# print("adding", tuple(tmp))
return True
break
else:
# return False
pass
# print(len(found))
first_removed = numbers[:]
del first_removed[i - 1]
if is_safe_1(first_removed, tolerance - 1):
return True
second_removed = numbers[:]
del second_removed[i]
if is_safe_1(second_removed, tolerance - 1):
return True
if len(found) > 0:
print(numbers, i, x)
print(first_removed, second_removed)
print(numbers, found)
return False
cur = x
return True
def is_safe_2(numbers, tolerance=0):
# Convert to differences
diffs = [0] # First element is zero, as there is no predecessor
for i in range(1, len(numbers)):
diffs.append(numbers[i] - numbers[i - 1])
print(numbers, diffs)
def solve(raw: str) -> int:
# Part 1
part1 = 0
part2 = 0
for line in raw.splitlines():
numbers = [int(x) for x in line.split(" ")]
if is_safe_2(numbers):
part1 += 1
if is_safe_2(numbers, 1):
part2 += 1
return (part1, part2)
# Test
with open("./2024/02/test.txt", "r") as f:
result = solve(f.read().strip())
print(result)
# Input
# with open("./2024/02/input.txt", "r") as f:
# result = solve(f.read().strip())
# print(result)

10
2024/03/findings.md Normal file
View File

@@ -0,0 +1,10 @@
# Day 3
Of course 1 min of regex freestyle (`mul\(\d+,\d+\)`) and it would have been done, i wanted to do it on my own.
NVM did it with Regex xD
Part 2 was fun, I basically computed an array of the `do()` and `don't()` index and simply looked up which one was further up.
## Other ways
I think a way more efficient way to go about it would to take more of a scan approach, where we keep track of the enabled/disabled status and parse as we go.

49
2024/03/main.py Normal file
View File

@@ -0,0 +1,49 @@
import re
re_mul = re.compile(r"mul\(\d+,\d+\)")
re_enable = re.compile(r"do\(\)")
re_disable = re.compile(r"don't\(\)")
def solve(raw: str) -> int:
# Part 1
part1 = 0
part2 = 0
enable = [m.end() for m in re_enable.finditer(raw)]
disable = [m.end() for m in re_disable.finditer(raw)]
for match in re_mul.finditer(raw):
# Part 1
[left, right] = match.group()[4:-1].split(",")
result = int(left) * int(right)
part1 += result
# Part 2
s = match.start()
last_enable = 0
for x in enable:
if x > s:
break
last_enable = x
last_disable = 0
for x in disable:
if x > s:
break
last_disable = x
disabled = last_disable != 0 and last_disable > last_enable
if disabled:
continue
part2 += result
return (part1, part2)
# Test
with open("./2024/03/test.txt", "r") as f:
result = solve(f.read().strip())
print(result)
# Input
with open("./2024/03/input.txt", "r") as f:
result = solve(f.read().strip())
print(result)

11
2024/04/findings.md Normal file
View File

@@ -0,0 +1,11 @@
# Day 4
## Part 1
I keep not learning. RTFM.
I started happily recursing into snakes of _XMAS_ letters, only to realise that only linear words are requested. Simple loop then.
## Part 2
Also quite easy. At first i messed up the cross, as i was checking for every angle, not only 45. And i had an error in the inverse of the direction of the cross, but nothing to hard.

130
2024/04/main.py Normal file
View File

@@ -0,0 +1,130 @@
from typing import List, Union, Tuple
Point = Tuple[int]
Path = Tuple[Point]
Paths = [Path]
ALL_DIRECTIONS = (
(1, 1),
(1, 0),
(1, -1),
(0, 1),
(0, -1),
(-1, 1),
(-1, 0),
(-1, -1),
)
LETTER_MAP = {"X": 0, "M": 1, "A": 2, "S": 3}
class Board:
rows: List[List[int]]
def __init__(self, rows: List[List[int]]):
self.rows = rows
self.max_y = len(self.rows)
self.max_x = len(self.rows[0])
def get(self, x: int, y: int) -> Union[None, int]:
if x < 0 or y < 0:
return None
if x >= self.max_x or y >= self.max_y:
return None
return self.rows[y][x]
# def get_neighbours(self, x: int, y: int, target: int) -> List[Point]:
# n = []
# for dx in range(x - 1, x + 2):
# for dy in range(y - 1, y + 2):
# if self.get(dx, dy) == target:
# n.append((dx, dy))
# return n
@staticmethod
def parse(raw: str):
rows = [[LETTER_MAP[letter] for letter in line] for line in raw.splitlines()]
return Board(rows)
# def find_maze(b: Board, path: Path, next_char: int) -> Paths:
# x, y = path[-1]
# print(f"Find {x} {y} n={next_char}")
# paths: Paths = []
# neighbours = b.get_neighbours(x, y, next_char)
# # Found last letter
# if next_char == 3:
# return [path + (n,) for n in neighbours]
# # Recurse
# for n in neighbours:
# p = path + (n,)
# sub_paths = find(b, p, next_char + 1)
# if len(sub_paths):
# paths = paths + sub_paths
# return paths
def find_line(b: Board, point: Point, direction: Point) -> Union[Path, None]:
x, y = point
dx, dy = direction
path: Path = point
for i in range(1, 4):
px = x + (dx * i)
py = y + (dy * i)
if b.get(px, py) != i:
return None
path = path + ((x, y),)
return path
def find_x(b: Board, point: Point) -> bool:
x, y = point
for dx, dy in ALL_DIRECTIONS:
# Check if is "M", then the opposite for "S" and the the perpendicular
if b.get(x + dx, y + dy) == 1:
if b.get(x - dx, y - dy) == 3:
# Found first "MAS", check perpendicular
p_dx = -dy
p_dy = dx
p_a = b.get(x + p_dx, y + p_dy)
p_b = b.get(x - p_dx, y - p_dy)
if (p_a == 1 and p_b == 3) or (p_a == 3 and p_b == 1):
return True
return False
def solve(raw: str) -> int:
# Part 1
part1 = []
part2 = 0
b = Board.parse(raw)
for x in range(b.max_x):
for y in range(b.max_y):
p = b.get(x, y)
if p == 0:
for direction in ALL_DIRECTIONS:
found = find_line(b, (x, y), direction)
if found:
part1.append(found)
if p == 2:
if find_x(b, (x, y)):
part2 += 1
return (len(part1), part2)
# Test
with open("./2024/04/test.txt", "r") as f:
result = solve(f.read().strip())
print(result)
# Input
with open("./2024/04/input.txt", "r") as f:
result = solve(f.read().strip())
print(result)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 122 KiB

20
2024/05/findings.md Normal file
View File

@@ -0,0 +1,20 @@
# Day 5
# Part 1
What a joy! First day where a bit of Computer Science might actually help.
So to me this looks like a [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph), since if there has to be an order, cycles cannot be allowed. Otherwise the problem could not be solved.
For not I just hope that it's one cohesive DAG, and not 2 or more. Technically there is a possibility that more than one, disjoined DAGs are defined by the rules. If this occurs, we'll see.
![DAG Image](./day_5_dag_test.svg)
We can see that there in this case there is one "start" and one "end". In this case `97` is the start and `13` the end. Again, there might be more than one "start" or "end".
My idea is to precompile an order so that `97=0` and `13=7` so that we then just map the numbers to their ascending number and check if the array is already sorted. Why? My assumption is that you do the work upfront and then it's efficient to check for each input.
The start and ends are easy, what about the _middle_ nodes? We can do a so called transitive reduction, simplifying the graph. Basically, if we know that `97` needs to be before `61` and `13` and at the same time `61` needs to be before `13`, we can delete the `97` before `13` link, as it's already specified going over `61`. Basically we eliminate all the "shortcuts". This gives us a nice line in this case.
![reduced DAG](./day_5_dag_reduced.svg)
_no code has been written until now, this might go terribly wrong 💩_

95
2024/05/main.py Normal file
View File

@@ -0,0 +1,95 @@
from typing import List, Union, Tuple, Set
from dataclasses import dataclass
import networkx as nx
import matplotlib.pyplot as plt
@dataclass
class Node:
id: int
links: List
@dataclass
class Manual:
rules: Tuple[int]
updates: Tuple[int]
def check_update(self, update: Tuple[int]) -> bool:
cur = 0
for n in update:
if n not in self.order:
continue
m = self.order[n]
if m < cur:
return False
cur = m
return True
def check(self):
total = 0
for update in self.updates:
valid = self.check_update(update)
if valid:
total += update[len(update) // 2]
return total
def build_rules(self):
unique: Set[int] = set()
for rule in self.rules:
unique.add(rule[0])
unique.add(rule[1])
nodes = {n: Node(id=n, links=[]) for n in unique}
for start, end in self.rules:
nodes[start].links.append(end)
# Test
G = nx.DiGraph()
for rule in self.rules:
G.add_edge(*rule)
print(G.number_of_nodes())
TR = nx.transitive_reduction(G)
subax1 = plt.subplot(121)
nx.draw(TR, with_labels=True, font_weight="bold")
plt.show()
sorted = list(nx.topological_sort(G))
print(sorted)
# Topological sorting
# https://cs.stackexchange.com/a/29133
order = {n: i for i, n in enumerate(sorted)}
self.order = order
@staticmethod
def parse(raw: str):
rules_raw, updates_raw = raw.strip().split("\n\n")
rules = [tuple((map(int, line.split("|")))) for line in rules_raw.splitlines()]
updates = [
tuple(map(int, line.split(","))) for line in updates_raw.splitlines()
]
return Manual(rules, updates)
def solve(raw: str) -> int:
# Part 1
part1 = 0
part2 = 0
m = Manual.parse(raw)
m.build_rules()
part1 = m.check()
return (part1, part2)
# Test
with open("./2024/05/test.txt", "r") as f:
result = solve(f.read().strip())
print(result)
# Input
with open("./2024/05/input.txt", "r") as f:
result = solve(f.read().strip())
print(result)

20
2024/06/findings.md Normal file
View File

@@ -0,0 +1,20 @@
# Day 5
# Part 1
What a joy! First day where a bit of Computer Science might actually help.
So to me this looks like a [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph), since if there has to be an order, cycles cannot be allowed. Otherwise the problem could not be solved.
For not I just hope that it's one cohesive DAG, and not 2 or more. Technically there is a possibility that more than one, disjoined DAGs are defined by the rules. If this occurs, we'll see.
![DAG Image](./day_5_dag_test.svg)
We can see that there in this case there is one "start" and one "end". In this case `97` is the start and `13` the end. Again, there might be more than one "start" or "end".
My idea is to precompile an order so that `97=0` and `13=7` so that we then just map the numbers to their ascending number and check if the array is already sorted. Why? My assumption is that you do the work upfront and then it's efficient to check for each input.
The start and ends are easy, what about the _middle_ nodes? We can do a so called transitive reduction, simplifying the graph. Basically, if we know that `97` needs to be before `61` and `13` and at the same time `61` needs to be before `13`, we can delete the `97` before `13` link, as it's already specified going over `61`. Basically we eliminate all the "shortcuts". This gives us a nice line in this case.
![reduced DAG](./day_5_dag_reduced.svg)
_no code has been written until now, this might go terribly wrong 💩_

90
2024/06/main.py Normal file
View File

@@ -0,0 +1,90 @@
from typing import List, Union, Tuple, Set, Self
from dataclasses import dataclass
type Point = Tuple[int, int]
TURN = {
# (-1,0) > (0, 1) -> (1, 0) -> (0, -1)
(-1, 0): (0, 1),
(0, 1): (1, 0),
(1, 0): (0, -1),
(0, -1): (-1, 0),
}
@dataclass
class Map:
fields: List[List[bool]]
position: Point
direction: Point
max_y: int = 0
max_x: int = 0
def __post_init__(self):
self.max_y = len(self.fields)
self.max_x = len(self.fields[0])
def next_step(self):
return (
self.position[0] + self.direction[0],
self.position[1] + self.direction[1],
)
def get(self, point: Point) -> Union[bool, None]:
y, x = point
try:
return self.fields[y][x]
except IndexError:
return None
def walk(self):
visited: Set[Point] = set()
while True:
visited.add(self.position)
next_step = self.next_step()
content = self.get(next_step)
if content is None:
# Exited map
break
if content:
# Turn
self.direction = TURN[self.direction]
next_step = self.next_step()
self.position = next_step
return len(visited)
@staticmethod
def parse(raw: str) -> Self:
fields = [[f == "#" for f in line] for line in raw.splitlines()]
# Find init
position = (0, 0)
direction = (-1, 0) # Up
for y, line in enumerate(raw.splitlines()):
for x, f in enumerate(line):
if f == "^":
print(f"FOUND! {x} {y}")
position = (y, x)
return Map(fields, position, direction)
def solve(raw: str) -> int:
# Part 1
part1 = 0
part2 = 0
m = Map.parse(raw)
part1 = m.walk()
return (part1, part2)
# Test
with open("./2024/06/test.txt", "r") as f:
result = solve(f.read().strip())
print(result)
# Input
with open("./2024/06/input.txt", "r") as f:
result = solve(f.read().strip())
print(result)