mirror of
https://github.com/cupcakearmy/advent-of-code.git
synced 2025-09-05 23:00:38 +00:00
2024
This commit is contained in:
45
2024/01/main.py
Normal file
45
2024/01/main.py
Normal 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
26
2024/02/findings.md
Normal 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
80
2024/02/main.py
Normal 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
10
2024/03/findings.md
Normal 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
49
2024/03/main.py
Normal 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
11
2024/04/findings.md
Normal 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
130
2024/04/main.py
Normal 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)
|
10
2024/05/day_5_dag_reduced.svg
Normal file
10
2024/05/day_5_dag_reduced.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 10 KiB |
33
2024/05/day_5_dag_test.svg
Normal file
33
2024/05/day_5_dag_test.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 122 KiB |
20
2024/05/findings.md
Normal file
20
2024/05/findings.md
Normal 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.
|
||||
|
||||

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

|
||||
|
||||
_no code has been written until now, this might go terribly wrong 💩_
|
95
2024/05/main.py
Normal file
95
2024/05/main.py
Normal 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
20
2024/06/findings.md
Normal 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.
|
||||
|
||||

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

|
||||
|
||||
_no code has been written until now, this might go terribly wrong 💩_
|
90
2024/06/main.py
Normal file
90
2024/06/main.py
Normal 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)
|
Reference in New Issue
Block a user