Compare commits

...

12 commits

Author SHA1 Message Date
b8409db06c Solve 2025 day 9 pt 2
Flood fill was too expensive, so I tried solving it with ray casting.

I believe it is the 2nd or 3rd time during 11 runs where it has been
requested.

wip

wip
2025-12-10 00:35:25 +01:00
ae676bf112 Solve 2025 day 9 pt 1
Pt 2 is almost there. Need a better flood fill
algorithm.
2025-12-10 00:34:12 +01:00
34fb92403e Solve 2025 day 8 pt 1-2
For the First time this season, pt 2 was not an epic
HOLY SHIIIT!!! facemelter.

I stared at my code in disbelief during pt 1 for several
minutes before I increased the test point from from 40
to 1000 and got the right answer.
2025-12-10 00:34:12 +01:00
8683885438 Solve 2025 day 7 pt 1-2
For part 1, BFS is used since there is no need to
visit each splitter more than once to count visited
splitters.

For part 2, the code initially removed the visited
check. This failed miserably, losing momentum aound
Y=54-56.

A recursive function with memoization solves it much
faster.
2025-12-10 00:34:12 +01:00
68be80a4d4 Solve 2025 day 6 pt 1-2
Lost 20 minutes in pt 1 not rembering that a print()
of a zip consumes it and makes it not loopable: had
I just used list(zip()) or removed the print, I would
have had the answer in a decent time frame.

Pt 2 was fun! I first experienced with ljust() and
rjust(), only to realize the input was mixed. Instead,
I threated the input as a grid.
2025-12-10 00:34:12 +01:00
2d3caced7f Solve 2025 day 5 pt 1-2
Part 2 was too hard for me, since I initially worked
with range(s, e). It quickly became out of hand.

A funnel method was instead used.
2025-12-10 00:34:12 +01:00
ac6b97590c Solve 2025 day 4 pt 1-2
Had a bug in my aoc lib, that lost me an hour for pt 1:
The ADJ dict has SW twice, and lacks NE.

The bug is at least 2 years old. It was introduced in 661f18dca4
and apparently has never worked. I guess it has not been used
up until now.

That being said, it was straight-forward. A new helper for
grid was used the first time, as well as an improved vdbg()
function.
2025-12-10 00:34:12 +01:00
c3c2dd5759 Fix aoc lib bug
Lost 1h in 2025 day 4 pt 1 for this. :D

Apparently, ADJ is not used that often.
2025-12-10 00:34:12 +01:00
4a070e827b Solve 2025 day 3 pt 1-2
For the first part, I used itertools.combinations
to find the highest pairs of batteries. And as
expected, that solution did not scale well for pt 2.

I figured out that reducing batteries until the top
most 12 (and 2) remained was the correct way to go.

the _maxj(line, C) function is the hive conclusion
from the solution mega thread. I really liked this
brilliant use of a while loop to exlude batteries.

 - The first char just skip the while loop. A char
   emptying the battery list also does this.
2025-12-10 00:34:12 +01:00
4927b78e59 Solve 2025 day 2 pt 1-2
I tried to solve it without regexp at first, failed
brutally. Cut the line count by 80% using a regexp
instead.

It was also funny to get all square roots.
2025-12-10 00:34:12 +01:00
56ca9d19c4 Solve 2025 day 1 pt 1-2
Brute force, since I was to newly awake to figure out
the smart solution.
2025-12-10 00:34:12 +01:00
f0f78fa25c Initiate Advent of Code 2025 2025-12-10 00:34:12 +01:00
13 changed files with 810 additions and 1 deletions

45
2025-python/README.md Normal file
View file

@ -0,0 +1,45 @@
# Advent of Code 2025
Solutions for #aoc2025 in Python 3 (3.13.4).
Programming setup:
- Lenovo Thinkpad T14
- OpenSUSE Tumbleweed with labwc
- Helix editor w/ Ruff LS
- Vivaldi
- Foot
## Help scripts
Display all solved puzzles:
python aoc.py
To bootstrap a new puzzle (creates `input/<day_no>.txt` and `output/day_<day_no>.py`):
python aoc.py <day_no> new
Manually copy the puzzle input from https://adventofcode.com and paste it in `input/<day_no>.txt`
to start coding.
wl-paste > input/<day_no>.txt
Solve separate puzzle (replace `XX` with the puzzle number):
python -m output.day_XX
Solve separate puzzle using stdin (replace `XX` with the puzzle number):
wl-paste | python -m output.day_XX
cat tmpfile | python -m output.day_XX
Execute separate puzzle on file save (replace `XX` with the puzzle number):
ls output/*.py | entr -c -s 'wlpaste | python -m output.day_XX'
ls output/*.py | entr -c -s 'cat tmpfile | python -m output.day_XX'
ls output/*.py | entr -c -r python -m output.day_XX
(requires `entr` and `wl-paste`, Mac users can instead use `pbpaste`. If you
prefer X at Linux, use `xclip -selection clipboard -o`).

118
2025-python/aoc.py Normal file
View file

@ -0,0 +1,118 @@
import sys
from pathlib import Path
def headline(n):
"""Print day number and name, followed by a ruler. Used by the answer decorator"""
print(f"\nDay {int(n)} - https://adventofcode.com/{year}/day/{int(n)}\n")
year = 2025
nostrip = []
try:
_, day_no, *name = sys.argv
except ValueError:
day_no = None
name = None
Path("./input").mkdir(parents=True, exist_ok=True)
Path("./output").mkdir(parents=True, exist_ok=True)
if day_no and name:
name = " ".join(name)
padded_no = day_no.zfill(2)
with open("output/day_{}.py".format(padded_no), "w") as s:
s.write(
f"""
import re
from collections import deque, Counter, defaultdict
from heapq import heappop, heappush
from itertools import compress, combinations, chain, permutations
from output import matrix, D, DD, ADJ, ints, sints, mhd, mdbg, vdbg, cw, ccw, bk
def solve(data):
p1 = None
p2 = None
return p1, p2
if __name__ == "__main__":
import os
# use dummy data
inp = \"\"\"
replace me
\"\"\".strip()
# uncomment to instead use stdin
# import sys; inp = sys.stdin.read().strip()
# uncomment to use AoC provided puzzle input
# with open("./input/{padded_no}.txt", "r") as f:
# inp = f.read().strip()
# uncomment to do initial data processing shared by part 1-2
p1, p2 = solve(inp)
print(p1)
os.system(f"echo {{p1}} | wl-copy")
# print(p2)
# os.system(f"echo {{p2}} | wl-copy")
# uncomment and replace 0 with actual output to refactor code
# and ensure nonbreaking changes
# assert p1 == 0
# assert p2 == 0
""".strip()
+ "\n"
)
exit(0)
print(
f"\n\033[95m\033[1mAdvent of Code {year}\033[0m"
"\n###################"
"\n\n\033[96mby Anders Englöf Ytterström\033[0m"
)
stars = 0
for i in [str(n).zfill(2) for n in range(1, 26)]:
if not day_no or day_no.zfill(2) == i:
try:
day = __import__(
"output.day_{}".format(i),
globals(),
locals(),
["solve"],
0,
)
with open(f"./input/{i}.txt", "r") as f:
data = f.read()
if int(i) not in nostrip:
data = data.strip()
headline(i)
try:
data = day.presolve(data)
except AttributeError:
pass
try:
p1, p2 = day.solve(data)
except AttributeError:
pass
if p1:
print(f" \033[92m1)\033[0m {p1}")
stars += 1
if p2:
print(f" \033[92m2)\033[0m {p2}")
stars += 1
except IOError:
pass
except ImportError:
pass
if not day_no:
print(f"\nStars: {stars}")
print("".join("*" if n < stars else "" for n in range(24)))
print("")

View file

@ -0,0 +1,257 @@
import re
from math import inf
from PIL import Image
# Directions/Adjacents for 2D matrices, in the order UP, RIGHT, DOWN, LEFT
D = [
(-1, 0),
(0, 1),
(1, 0),
(0, -1),
]
Di = [
(-1, -1),
(-1, 1),
(1, -1),
(1, 1),
]
# Directions for 2D matrices, as a dict with keys U, R, D, L
DD = {
"U": (-1, 0),
"R": (0, 1),
"D": (1, 0),
"L": (0, -1),
}
DDa = {
"^": (-1, 0),
">": (0, 1),
"v": (1, 0),
"<": (0, -1),
}
# Adjacent relative positions including diagonals for 2D matrices, in the order NW, N, NE, W, E, SW, S, SE
ADJ = [
(-1, -1),
(-1, 0),
(-1, 1),
(0, -1),
(0, 1),
(1, 1),
(1, 0),
(1, -1),
]
def ints(s):
"""Extract all integers from a string"""
return [int(n) for n in re.findall(r"\d+", s)]
def sints(s):
"""Extract all signed integers from a string"""
return [int(n) for n in re.findall(r"-?\d+", s)]
def mhd(a, b):
"""Calculates the Manhattan distance between 2 positions in the format (y, x) or (x, y)"""
ar, ac = a
br, bc = b
return abs(ar - br) + abs(ac - bc)
def grid(d, o="#"):
"""Transform a string into an iterable matrix. Returns the matrix, row count and col count"""
m = [tuple(r) for r in d.split()]
return set([(r, c) for c in range(len(m[0])) for r in range(len(m)) if m[r][c] == o])
def matrix(d):
"""Transform a string into an iterable matrix. Returns the matrix, row count and col count"""
m = [tuple(r) for r in d.split()]
return m, len(m), len(m[0])
def mdbg(m):
"""Print-debug a matrix"""
for r in m:
print("".join(r))
def vdbg(seen, mask=("#", "."), M=None):
"""Print-debug visited positions of a matrix"""
t = inf
l = inf
b = 0
r = 0
for y, x in seen:
t = min(t, y)
r = max(r, x)
b = max(b, y)
l = min(l, x)
H = b + 1
W = r + 1
osr = 0 #t
osc = 0 #l
C, Z = mask
def _m(r, c):
return M[r][c] if M else Z
O = []
for r in range(H):
O.append("".join([C if (r + osr, c + osc) in seen else _m(r, c) for c in range(W)]))
print("\n".join(O))
def svg(seen):
"""Print-debug visited positions of a matrix"""
t = inf
l = inf
b = 0
r = 0
rects = []
for y, x in seen:
t = min(t, y)
r = max(r, x)
b = max(b, y)
l = min(l, x)
H = b - t + 1
W = r - l + 1
print(t, r, b, l)
im = Image.new(mode="RGB", size=(W,H), color=(255,255,255))
for y, x in seen:
im.putpixel((x-l, y-t), (0, 0, 0, 255))
im.save("aoc.png")
for y, x in seen:
rects.append(f"<rect x='{x-l}' y='{y-t}' width='1' fill='black' stroke='black' stroke-width='10' height='1' />")
with open("svg.svg", "w") as f:
f.write(f"""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">
{"".join(rects)}
</svg>
""".strip())
def vvdbg(seen, h, w):
"""Print-debug visited positions of a matrix, with values"""
for r in range(h):
print("".join([seen[(r, c)] if (r, c) in seen else "." for c in range(w)]))
def cw(y, x):
"""Flip a (y, x) direction counterwise: U->R, R->D, D->L, L->U.
>>> cw(-1, 0)
(0, 1)
>>> cw(0, 1)
(1, 0)
>>> cw(1, 0)
(0, -1)
>>> cw(0, -1)
(-1, 0)
"""
return (x, y) if y == 0 else (x, -y)
def ccw(y, x):
"""Flip a (y, x) direction counterwise: U->L, L->D, D->R, R->U.
>>> ccw(-1, 0)
(0, -1)
>>> ccw(0, -1)
(1, 0)
>>> ccw(1, 0)
(0, 1)
>>> ccw(0, 1)
(-1, 0)
"""
return (x, y) if x == 0 else (-x, y)
def bfs(S, E=None):
"""BFS algorithm, equal weighted nodes"""
seen = set()
q = [(S, 0)]
g = {} # graph, required to be provided at some point
while q:
m, w = q.pop(0)
if m in seen:
continue
seen.add(m)
# investigate here
for s in g[m]:
q.append((s, w + 1))
# return insights
def mhd_search(r, c, R=20):
"""returns all coords that are within R manhattan distance from (r,c)"""
p = set()
for d in range(1, R + 1):
p.add((r, c + d))
p.add((r, c - d))
p.add((r + d, c))
p.add((r - d, c))
for dd in range(d):
p.add((r - dd, c - d + dd))
p.add((r + dd, c - d + dd))
p.add((r - dd, c - dd + d))
p.add((r + dd, c - dd + d))
return p
def dijkstras(grid, start, target):
"""
1. Create an array that holds the distance of each vertex from the starting
vertex. Initially, set this distance to infinity for all vertices except
the starting vertex which should be set to 0.
2. Create a priority queue (heap) and insert the starting vertex with its
distance of 0.
3. While there are still vertices left in the priority queue, select the vertex
with the smallest recorded distance from the starting vertex and visit its
neighboring vertices.
4. For each neighboring vertex, check if it is visited already or not. If it
isnt visited yet, calculate its tentative distance by adding its weight
to the smallest distance found so far for its parent/previous node
(starting vertex in case of first-level vertices).
5. If this tentative distance is smaller than previously recorded value
(if any), update it in our distances array.
6. Finally, add this visited vertex with its updated distance to our priority
queue and repeat step-3 until we have reached our destination or exhausted
all nodes.
"""
import heapq
target = max(grid)
seen = set()
queue = [(start, 0)]
while queue:
cost, pos, direction, steps = heapq.heappop(queue)
y, x = pos
dy, dx = direction
if pos == target:
return cost
if ((pos, "and stuff")) in seen:
continue
seen.add((pos, "and stuff"))
neighbors = []
for n in neighbors:
heapq.heappush(queue, ("stuffs"))
return -1
def bk(graph, p, r=set(), x=set()):
"""Bron-Kerbosch algoritm, no pivot: https://en.wikipedia.org/wiki/Bron%E2%80%93Kerbosch_algorithm"""
if not p and not x:
yield r
while p:
v = p.pop()
yield from bk(graph, p & set(graph[v]), r | {v}, x & graph[v])
x.add(v)

View file

@ -0,0 +1,29 @@
def solve(data):
d = 50
p1 = 0
p2 = 0
for s in data.split():
sf0 = d == 0
m, T = (-1, 99) if s[0] == "L" else (1, 1)
for _ in range(int(s[1:])):
d = (d + m) % 100
if d == T:
if sf0:
sf0 = False
continue
p2 += 1
p1 += d == 0
return p1, p1 + p2
if __name__ == "__main__":
with open("./input/01.txt", "r") as f:
inp = f.read().strip()
p1, p2 = solve(inp)
print(p1)
print(p2)
assert p1 == 1195
assert p2 == 6770

View file

@ -0,0 +1,35 @@
import re
from output import ints
def solve(data):
p1 = set()
p2 = set()
R = re.compile(r"^(\w+)\1+$")
for line in data.split(","):
a, b = ints(line)
for n in range(a, b + 1):
s = str(n)
ls = len(s)
for seq in re.findall(R, s):
sqrts = [i for i in range(1, ls + 1) if ls % i == 0]
for t in sqrts:
if "".join([seq] * t) == s:
if t == 2:
p1.add(n)
p2.add(n)
return sum(p1), sum(p2)
if __name__ == "__main__":
with open("./input/02.txt", "r") as f:
inp = f.read().strip()
p1, p2 = solve(inp)
print(p1)
print(p2)
assert p1 == 38437576669
assert p2 == 49046150754

View file

@ -0,0 +1,31 @@
def solve(data):
p1 = 0
p2 = 0
for line in data.splitlines():
p1 += _maxj(line, 2)
p2 += _maxj(line, 12)
return p1, p2
def _maxj(line, C):
toexcl = len(line) - C
batt = []
for c in line:
while toexcl and batt and batt[-1] < c:
toexcl -= 1
batt.pop()
batt.append(c)
return sum(10**x * int(y) for x, y in zip(range(C - 1, -1, -1), batt))
if __name__ == "__main__":
with open("./input/03.txt", "r") as f:
inp = f.read().strip()
p1, p2 = solve(inp)
print(p1)
print(p2)
assert p1 == 17430
assert p2 == 171975854269367

View file

@ -0,0 +1,31 @@
from output import grid, ADJ
def solve(data):
p1 = 0
p2 = set()
G = grid(data, o="@")
while True:
for r, c in G:
if sum((r + dy, c + dx) in G for dy, dx in ADJ) < 4:
p2.add((r, c))
if p1 == 0:
p1 = len(p2)
if not G & p2:
break
G = G - p2
p2 = len(p2)
return p1, p2
if __name__ == "__main__":
with open("./input/04.txt", "r") as f:
inp = f.read().strip()
p1, p2 = solve(inp)
print(p1)
print(p2)
assert p1 == 1493
assert p2 == 9194

View file

@ -0,0 +1,31 @@
from output import ints
def solve(data):
p1 = None
p2 = 0
ranges, ids = data.split("\n\n")
ids = ints(ids)
R = [ints(line) for line in ranges.split()]
p1 = sum(any(s <= n <= e for s, e in R) for n in ids)
c = 0
for s, e in sorted(R):
s = max(c + 1, s)
if s <= e:
p2 += e - s + 1
c = max(e, c)
return p1, p2
if __name__ == "__main__":
with open("./input/05.txt", "r") as f:
inp = f.read().strip()
p1, p2 = solve(inp)
print(p1)
print(p2)
assert p1 == 509
assert p2 == 336790092076620

View file

@ -0,0 +1,40 @@
import re
from math import prod
def solve(data):
p1 = 0
p2 = 0
rows = data.splitlines()
J = max(len(r) for r in rows) + 1
rows = [r.ljust(J) for r in rows]
ops = rows.pop()
s = 0
for ows in re.findall(r"(\S\s+)", ops):
o, *ws = ows
e = s + len(ws)
col = [ns for ns in [r[s:e] for r in rows]]
s += len(ows)
col1 = [int(r) for r in col]
col2 = [int("".join([nsc for nsc in ns if nsc.isdigit()])) for ns in zip(*col)]
match o:
case "*":
p1 += prod(col1)
p2 += prod(col2)
case "+":
p1 += sum(col1)
p2 += sum(col2)
return p1, p2
if __name__ == "__main__":
with open("./input/06.txt", "r") as f:
inp = f.read().strip()
p1, p2 = solve(inp)
print(p1)
print(p2)
assert p1 == 5524274308182
assert p2 == 8843673199391

View file

@ -0,0 +1,50 @@
import functools
from output import grid
def solve(data):
G = grid(data, o="^")
p1 = set()
p2 = 0
H = len(data.split())
W = len(data.split()[0])
S = (0, data.split()[0].index("S"))
Q = [S]
while Q:
y, x = Q.pop(0)
if y == H:
continue
if (y, x) in p1:
continue
if (y, x) in G:
Q.append((y, x - 1))
Q.append((y, x + 1))
p1.add((y, x))
else:
Q.append((y + 1, x))
p1 = len(p1)
@functools.cache
def _timelines(p):
y, x = p
if not 0 <= y < H or not 0 <= x < W:
return 1
if p in G:
return _timelines((y, x - 1)) + _timelines((y, x + 1))
return _timelines((y + 1, x))
p2 = _timelines(S)
return p1, p2
if __name__ == "__main__":
with open("./input/07.txt", "r") as f:
inp = f.read().strip()
p1, p2 = solve(inp)
print(p1)
print(p2)
assert p1 == 1539
assert p2 == 6479180385864

View file

@ -0,0 +1,58 @@
from math import sqrt
from itertools import combinations
from output import ints
def solve(data):
Di = sorted(
map(_euclidan_distance, combinations(data.splitlines(), r=2)),
key=lambda r: r[1],
)
C = []
S = len(data.splitlines())
IP = 1000
for i, pq in enumerate(Di, start=1):
pq, _ = pq
c = set(pq)
p, q = pq
rmq = []
nothing = False
for j in C:
if p in j and q in j:
nothing = True
continue
if p in j or q in j:
c = c | j
rmq.append(j)
C = [e for e in C if e not in rmq]
if c == pq and nothing:
continue
if len(c) == S:
p2 = ints(p)[0] * ints(q)[0]
break
C = [c] + C
if i == IP:
a, b, c = sorted(list(map(len, C)), reverse=True)[:3]
p1 = a * b * c
return p1, p2
def _euclidan_distance(pq):
p, q = pq
p1, p2, p3 = ints(p)
q1, q2, q3 = ints(q)
return (p, q), sqrt((p1 - q1) ** 2 + (p2 - q2) ** 2 + (p3 - q3) ** 2)
if __name__ == "__main__":
with open("./input/08.txt", "r") as f:
inp = f.read().strip()
p1, p2 = solve(inp)
print(p1)
print(p2)
assert p1 == 84968
assert p2 == 8663467782

View file

@ -0,0 +1,84 @@
from math import inf
from itertools import combinations
from output import ints
def solve(data):
p1 = 0
p2 = 0
A = [tuple(ints(pos)) for pos in data.splitlines()]
T = inf
R = 0
B = 0
L = inf
for r, c in A:
T = min(r, T)
R = max(c, R)
B = max(r, B)
L = min(c, L)
S = A[0]
V = set()
W = set()
while True:
V.add(S)
W.add(S)
y, x = S
he = [(r, c) for r, c in A if r == y and (r, c) not in V]
ve = [(r, c) for r, c in A if c == x and (r, c) not in V]
if not he and not ve:
E = A[0]
elif he and ve:
E = min(min(he), min(ve))
elif he and not ve:
E = min(he)
else:
E = min(ve)
y2, x2 = E
for r in range(min(y, y2), max(y, y2) + 1):
for c in range(min(x, x2), max(x, x2) + 1):
W.add((r, c))
if E == A[0]:
break
S = E
V = V | W
for a, b in combinations(A, r=2):
y1, x1 = a
y2, x2 = b
x = abs(x1 - x2) + 1
y = abs(y1 - y2) + 1
p1 = max(p1, x * y)
if _within(W, a, b, T, R, B, L):
p2 = max(p2, max(p2, x * y))
return p1, p2
def _within(W, a, b, T, R, B, L):
y1, x1 = a
y2, x2 = b
for r in range(min(y1, y2) + 1, max(y1, y2)):
for c in range(min(x1, x2) + 1, max(x1, x2)):
if (r, c) in W:
continue
for s, e in [(T, r), (r, B)]:
z = sum((nr, c) in W for nr in range(s, e))
if z and z % 2 == 0:
return False
for s, e in [(L, c), (c, R)]:
z = sum((r, nc) in W for nc in range(s, e))
if z and z % 2 == 0:
return False
return True
if __name__ == "__main__":
with open("./input/09.txt", "r") as f:
inp = f.read().strip()
p1, p2 = solve(inp)
print(p1)
print(p2)
assert p1 == 4749838800
assert p2 == 1624057680

View file

@ -8,7 +8,7 @@ RUN pip install waitress==3.0.2
FROM reqs AS app FROM reqs AS app
RUN mkdir /app/templates RUN mkdir /app/templates
COPY *.jinja2 /app/templates COPY templates/*.jinja2 /app/templates
COPY app.py app.py COPY app.py app.py
ENV AOC_TOKEN= ENV AOC_TOKEN=