40 Commits

Author SHA1 Message Date
bc6f3dc8c6 2025 day 10 part 2 in Rust
Using a library. I'm not happy about it but also I have thought about
this enough.
2025-12-11 21:46:15 +01:00
a8a1c85498 2025 day 11 in Python 2025-12-11 09:44:39 +01:00
ff8b8dac0c 2025 day 10 part 1 in Rust 2025-12-10 08:39:51 +01:00
51ca5db42e 2025 day 9 part 2 in TS 2025-12-09 19:44:31 +01:00
e60bfbf8c4 2025 day 9 part 1 in TypeScript 2025-12-09 08:41:13 +01:00
768a3ff10b One pointless index removed 2025-12-08 22:07:02 +01:00
c3d3116f8a Improvise heap sort for partial sorting 2025-12-08 21:51:17 +01:00
13592a60ee 2025 day 8 in Go 2025-12-08 21:21:24 +01:00
be6c3d37ea Minor cleanup
Avoid repeatedly creating sets that live for almost no time, as well as
some readability fixes
2025-12-07 22:38:39 +01:00
ff2c86a11b Missing compilation instructions 2025-12-07 22:26:25 +01:00
a737a35227 2025 day 7 part 2 in Haskell 2025-12-07 16:51:24 +01:00
29e02c819e Implement 2025 day 7 part 1 in Haskell 2025-12-07 15:12:21 +01:00
a01eb547d1 2025 day 6 in PHP 2025-12-06 12:42:32 +01:00
1485eb3cd5 Implement 2025 day 5 in C 2025-12-05 08:52:58 +01:00
0a7ec71a97 Very ugly part 2 2025-12-04 22:35:12 +01:00
f2204e554b Moderately cursed 2025 day 4 part 1 2025-12-04 09:29:39 +01:00
6f4b02af33 Implement 2025 day 3 in bash 2025-12-03 09:54:40 +01:00
7e23cf94a6 2025 day 2 part 2 in Terraform 2025-12-02 13:39:20 +01:00
f132842b5c Forgot to add 2025-12-02 09:58:16 +01:00
027a7bdde6 Attempt at part two, OOM 2025-12-02 09:55:37 +01:00
2277721010 2025 day 2 part 1 2025-12-02 09:36:44 +01:00
5e9a24c8d7 Document Nix solution 2025-12-01 21:13:50 +01:00
1468c87347 Implement 2025 day 1 part 2 2025-12-01 20:47:21 +01:00
1dc59c18eb Solve 2025 day 1 part 1 2025-12-01 08:59:13 +01:00
dbcbd15103 Prepare 2025 2025-12-01 08:08:13 +01:00
b6aafa1b27 Slightly cleaner 2025-02-03 23:10:53 +01:00
1f7108be47 Euclid's algorithm 2025-02-03 23:02:24 +01:00
48fb8cf8c9 Remove unnecesary duplication 2025-02-03 20:33:49 +01:00
f9416db251 Implement day 8 2024 in Terraform 2025-01-29 00:43:14 +01:00
6941f2b2d2 Rely more on type coercion 2025-01-21 21:40:07 +01:00
dc92b65830 Inline loop 2024-12-27 21:45:36 +01:00
5c030d5272 Implement 2024 day 14 part 1 in Terraform 2024-12-27 21:14:28 +01:00
d07bb9235b Implement 2024 day 13 in terraform 2024-12-27 21:08:47 +01:00
b23676bf04 Implement 2024 day 21 part 2
Invent a unit test for it too, because why not, it's Christmas
2024-12-26 16:43:15 +01:00
40632c8114 Implement 2024 day 21 part 1 2024-12-26 15:55:06 +01:00
073b576fd8 Implement 2024 day 25 in Terraform 2024-12-26 15:13:46 +01:00
f3a3e1fca3 More numpy for speed 2024-12-25 10:34:40 +01:00
4a479c1646 Implement 2024 day 25 2024-12-25 10:25:48 +01:00
b41571949e Fix problem by adding more lookback 2024-12-24 21:17:49 +01:00
e949ce9932 Sort of functional implementation of 2024 day 24 2024-12-24 20:56:12 +01:00
74 changed files with 3114 additions and 75 deletions

View File

@@ -1,43 +0,0 @@
on:
- push
name: Advent of Code 2024
jobs:
ci:
strategy:
matrix:
python-version:
- "3.12"
- "3.13"
name: Continuous Integration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: "2024/uv.lock"
- name: Check formatting
working-directory: "2024"
run: >
uv run ruff format --check
- name: Run lints
working-directory: "2024"
run: >
uv run ruff check
- name: Run tests
working-directory: "2024"
run: >
uv run pytest tests

15
2024/bonus/24todot.py Normal file
View File

@@ -0,0 +1,15 @@
import fileinput
print("digraph day24 {")
for line in fileinput.input():
parts = line.split(" ")
if len(parts) != 5:
continue
first, op, second, _, result = parts
print(f'{first}{second}{op} [label="{op}"];')
print(f"{first} -> {first}{second}{op} -> {result};")
print(f"{second} -> {first}{second}{op};")
print("}")

View File

@@ -6,8 +6,8 @@ locals {
cleaned_input = replace(var.input, "/ +/", " ")
lines = split("\n", trim(local.cleaned_input, "\n"))
lines_split = [for line in local.lines : split(" ", line)]
left = [for line in local.lines_split : parseint(line[0], 10)]
right = [for line in local.lines_split : parseint(line[1], 10)]
left = [for line in local.lines_split : tonumber(line[0])]
right = [for line in local.lines_split : tonumber(line[1])]
left_sorted = sort(local.left)
right_sorted = sort(local.right)

View File

@@ -3,7 +3,7 @@ variable "input" {
}
locals {
reports = [for line in split("\n", trim(var.input, "\n")) : [for num in split(" ", line) : parseint(num, 10)]]
reports = [for line in split("\n", trim(var.input, "\n")) : split(" ", line)]
}
module "part1_valid" {

View File

@@ -1,21 +0,0 @@
variable "update" {
type = list(number)
}
variable "disallow_rules" {
type = map(list(number))
}
locals {
not_disallowed = alltrue([
for i in range(1, length(var.update)) :
!contains(
flatten([for j in range(i) : lookup(var.disallow_rules, var.update[j], [])]),
var.update[i]
)
])
}
output "valid" {
value = local.not_disallowed ? var.update[floor(length(var.update) / 2)] : 0
}

View File

@@ -8,16 +8,18 @@ locals {
disallow_rules = { for rule in local.rules : rule[1] => rule[0]... }
updates = [for update_line in split("\n", local.parts[1]) : [for v in split(",", update_line) : tonumber(v)]]
}
module "is_valid" {
source = "./is_correct"
count = length(local.updates)
update = local.updates[count.index]
disallow_rules = local.disallow_rules
scores = [
for update in local.updates :
alltrue([
for i in range(1, length(update)) :
!contains(
flatten([for j in range(i) : lookup(local.disallow_rules, update[j], [])]),
update[i]
)
]) ? update[floor(length(update) / 2)] : 0]
}
output "part1" {
value = sum(module.is_valid[*].valid)
value = sum(local.scores[*])
}

View File

@@ -0,0 +1,52 @@
variable "width" {
type = number
}
variable "height" {
type = number
}
variable "antennae" {
type = list(tuple([number, number]))
}
locals {
pairs = concat([
for i in range(length(var.antennae)) :
[
for j in range(i + 1, length(var.antennae)) : [var.antennae[i], var.antennae[j]]
]
]...)
}
module "pair" {
source = "./pair"
count = length(local.pairs)
first = local.pairs[count.index][0]
second = local.pairs[count.index][1]
width = var.width
height = var.height
}
output "nodes1" {
value = setunion([
for i in range(length(local.pairs)) :
[
for v in module.pair[i].nodes1 :
v
if v[0] >= 0 && v[0] < var.width && v[1] >= 0 && v[1] < var.height
]
]...)
}
output "nodes2" {
value = setunion([
for i in range(length(local.pairs)) :
[
for v in module.pair[i].nodes2 :
v
if v[0] >= 0 && v[0] < var.width && v[1] >= 0 && v[1] < var.height
]
]...)
}

View File

@@ -0,0 +1,52 @@
variable "first" {
type = tuple([number, number])
}
variable "second" {
type = tuple([number, number])
}
variable "width" {
type = number
}
variable "height" {
type = number
}
locals {
dx = var.second[0] - var.first[0]
dy = var.second[1] - var.first[1]
# sort() doesn't work as it turns the numbers into strings
gcd0 = abs(local.dx) < abs(local.dy) ? [abs(local.dx), abs(local.dy)] : [abs(local.dy), abs(local.dx)]
# Do as many iterations as necessary.
gcd1 = local.gcd0[0] == 0 ? local.gcd0 : [local.gcd0[1] % local.gcd0[0], local.gcd0[0]]
gcd2 = local.gcd1[0] == 0 ? local.gcd1 : [local.gcd1[1] % local.gcd1[0], local.gcd1[0]]
gcd3 = local.gcd2[0] == 0 ? local.gcd2 : [local.gcd2[1] % local.gcd2[0], local.gcd2[0]]
gcd4 = local.gcd3[0] == 0 ? local.gcd3 : [local.gcd3[1] % local.gcd3[0], local.gcd3[0]]
gcd5 = local.gcd4[0] == 0 ? local.gcd4 : [local.gcd4[1] % local.gcd4[0], local.gcd4[0]]
gcd6 = local.gcd5[0] == 0 ? local.gcd5 : [local.gcd5[1] % local.gcd5[0], local.gcd5[0]]
gcd7 = local.gcd6[0] == 0 ? local.gcd6 : [local.gcd6[1] % local.gcd6[0], local.gcd6[0]]
gcd8 = local.gcd7[0] == 0 ? local.gcd7 : [local.gcd7[1] % local.gcd7[0], local.gcd7[0]]
gcd9 = local.gcd8[0] == 0 ? local.gcd8 : [local.gcd8[1] % local.gcd8[0], local.gcd8[0]]
gcd10 = local.gcd9[0] == 0 ? local.gcd9 : [local.gcd9[1] % local.gcd9[0], local.gcd9[0]]
# 10 iterations should cover numbers up to 55, which is more than the width/height
gcd = local.gcd10[1]
}
output "nodes1" {
value = [
[var.first[0] - local.dx, var.first[1] - local.dy],
[var.second[0] + local.dx, var.second[1] + local.dy],
]
}
output "nodes2" {
value = concat(
[for i in range(max(var.width, var.height)) : [var.first[0] - i * local.dx / local.gcd, var.first[1] - i * local.dy / local.gcd]],
[for i in range(max(var.width, var.height)) : [var.second[0] + i * local.dx / local.gcd, var.second[1] + i * local.dy / local.gcd]]
)
}

39
2024/bonus/day08/main.tf Normal file
View File

@@ -0,0 +1,39 @@
variable "input" {
type = string
}
locals {
lines = split("\n", chomp(var.input))
height = length(local.lines)
width = length(local.lines[0])
antennae = concat([
for y in range(local.height) :
[
for x in range(local.width) :
[substr(local.lines[y], x, 1), x, y]
if substr(local.lines[y], x, 1) != "."
]
]...)
by_freq = {
for antenna in local.antennae :
antenna[0] => [antenna[1], antenna[2]]...
}
}
module "freq" {
source = "./freq"
for_each = local.by_freq
width = local.width
height = local.height
antennae = each.value
}
output "part1" {
value = length(setunion([for _, v in module.freq : v.nodes1]...))
}
output "part2" {
value = length(setunion([for _, v in module.freq : v.nodes2]...))
}

31
2024/bonus/day13/main.tf Normal file
View File

@@ -0,0 +1,31 @@
variable "input" {
type = string
}
locals {
machines = regexall(
"Button A: X\\+(\\d+), Y\\+(\\d+)\nButton B: X\\+(\\d+), Y\\+(\\d+)\nPrize: X=(\\d+), Y=(\\d+)",
var.input
)
}
module "solve1" {
source = "./solve"
machines = local.machines
}
module "solve2" {
source = "./solve"
machines = [
for machine in local.machines :
[machine[0], machine[1], machine[2], machine[3], 10000000000000 + machine[4], 10000000000000 + machine[5]]
]
}
output "part1" {
value = module.solve1.solutions
}
output "part2" {
value = module.solve2.solutions
}

View File

@@ -0,0 +1,34 @@
variable "machines" {
type = list(list(number))
}
locals {
a_substitutions = [
for machine in var.machines :
[-machine[2] / machine[0], machine[4] / machine[0]]
]
b_equations = [
for i in range(length(var.machines)) :
[
var.machines[i][3] + local.a_substitutions[i][0] * var.machines[i][1],
var.machines[i][5] - local.a_substitutions[i][1] * var.machines[i][1]
]
]
b = [for eq in local.b_equations : floor(eq[1] / eq[0] + 0.5)]
a = [
for i in range(length(var.machines)) :
floor((var.machines[i][4] - local.b[i] * var.machines[i][2]) / var.machines[i][0] + 0.5)
]
}
output "solutions" {
value = sum([
for i in range(length(var.machines)) :
3 * local.a[i] + local.b[i]
if var.machines[i][0] * local.a[i] + var.machines[i][2] * local.b[i] == var.machines[i][4]
&& var.machines[i][1] * local.a[i] + var.machines[i][3] * local.b[i] == var.machines[i][5]
])
}

33
2024/bonus/day14/main.tf Normal file
View File

@@ -0,0 +1,33 @@
variable "input" {
type = string
}
variable "width" {
type = number
default = 101
}
variable "height" {
type = number
default = 103
}
locals {
lines = regexall("p=(-?\\d+),(-?\\d+) v=(-?\\d+),(-?\\d+)", var.input)
positions = [
for line in local.lines :
[
((line[0] + 100 * line[2]) % var.width + var.width) % var.width,
((line[1] + 100 * line[3]) % var.height + var.height) % var.height,
]
]
q1 = length([for pos in local.positions : pos if pos[0] < floor(var.width / 2) && pos[1] < floor(var.height / 2)])
q2 = length([for pos in local.positions : pos if pos[0] > floor(var.width / 2) && pos[1] < floor(var.height / 2)])
q3 = length([for pos in local.positions : pos if pos[0] < floor(var.width / 2) && pos[1] > floor(var.height / 2)])
q4 = length([for pos in local.positions : pos if pos[0] > floor(var.width / 2) && pos[1] > floor(var.height / 2)])
}
output "part1" {
value = local.q1 * local.q2 * local.q3 * local.q4
}

23
2024/bonus/day25/main.tf Normal file
View File

@@ -0,0 +1,23 @@
variable "input" {
type = string
}
locals {
blocks = split("\n\n", chomp(var.input))
heights = [
for block in local.blocks : [
for i in range(5) : length([
for line in split("\n", block) : line if substr(line, i, 1) == "#"
])
]
]
locks = [for i in range(length(local.blocks)) : local.heights[i] if startswith(local.blocks[i], "#####")]
keys = [for i in range(length(local.blocks)) : local.heights[i] if !startswith(local.blocks[i], "#####")]
combined = concat([for lock in local.locks : [for key in local.keys : [for i in range(5) : lock[i] + key[i] <= 7]]]...)
}
output "part1" {
value = length([for combination in local.combined : combination if alltrue(combination)])
}

View File

@@ -66,6 +66,19 @@ output "day05_1" {
value = module.day05.part1
}
module "day08" {
source = "./day08"
input = file("../inputs/08.txt")
}
output "day08_1" {
value = module.day08.part1
}
output "day08_2" {
value = module.day08.part2
}
module "day11" {
source = "./day11"
input = file("../inputs/11.txt")
@@ -79,6 +92,28 @@ output "day11_2" {
value = module.day11.part2
}
module "day13" {
source = "./day13"
input = file("../inputs/13.txt")
}
output "day13_1" {
value = module.day13.part1
}
output "day13_2" {
value = module.day13.part2
}
module "day14" {
source = "./day14"
input = file("../inputs/14.txt")
}
output "day14_1" {
value = module.day14.part1
}
module "day19" {
source = "./day19"
input = file("../inputs/19.txt")
@@ -87,3 +122,12 @@ module "day19" {
output "day19_1" {
value = module.day19.part1
}
module "day25" {
source = "./day25"
input = file("../inputs/25.txt")
}
output "day25_1" {
value = module.day25.part1
}

View File

@@ -131,6 +131,29 @@ run "day5_1" {
}
}
run "day08" {
command = plan
module {
source = "./day08"
}
variables {
input = file("../tests/samples/08.txt")
}
assert {
condition = output.part1 == 14
error_message = "Part1 output is wrong"
}
assert {
condition = output.part2 == 34
error_message = "Part1 output is wrong"
}
}
run "day11" {
command = plan
@@ -148,6 +171,42 @@ run "day11" {
}
}
run "day13" {
command = plan
module {
source = "./day13"
}
variables {
input = file("../tests/samples/13.txt")
}
assert {
condition = output.part1 == 480
error_message = "Part1 output is wrong"
}
}
run "day14" {
command = plan
module {
source = "./day14"
}
variables {
input = file("../tests/samples/14.txt")
height = 7
width = 11
}
assert {
condition = output.part1 == 12
error_message = "Part1 output is wrong"
}
}
run "day19" {
command = plan
@@ -164,3 +223,20 @@ run "day19" {
error_message = "Part1 output is wrong"
}
}
run "day25" {
command = plan
module {
source = "./day25"
}
variables {
input = file("../tests/samples/25.txt")
}
assert {
condition = output.part1 == 3
error_message = "Part1 output is wrong"
}
}

159
2024/src/aoc/days/day21.py Normal file
View File

@@ -0,0 +1,159 @@
import functools
from collections import Counter, defaultdict
from . import SeparateRunner
NUMPAD = {
"A": (3, 2),
"0": (3, 1),
"1": (2, 0),
"2": (2, 1),
"3": (2, 2),
"4": (1, 0),
"5": (1, 1),
"6": (1, 2),
"7": (0, 0),
"8": (0, 1),
"9": (0, 2),
}
DIRPAD = {
"A": (0, 2),
"^": (0, 1),
"<": (1, 0),
"v": (1, 1),
">": (1, 2),
}
@functools.cache
def shortest_numpad(from_: str, to: str) -> list[str]:
inverse = set(NUMPAD.values())
ay, ax = NUMPAD[from_]
by, bx = NUMPAD[to]
dx, dy = bx - ax, by - ay
sx = "<" if dx < 0 else ">"
sy = "^" if dy < 0 else "v"
if dx > 0 and (by, ax) in inverse or (ay, bx) not in inverse:
return abs(dy) * sy + abs(dx) * sx + "A"
else:
return abs(dx) * sx + abs(dy) * sy + "A"
@functools.cache
def shortest_dirpad(from_: str, to: str) -> str:
inverse = set(DIRPAD.values())
ay, ax = DIRPAD[from_]
by, bx = DIRPAD[to]
dx, dy = bx - ax, by - ay
sx = "<" if dx < 0 else ">"
sy = "^" if dy < 0 else "v"
if dx > 0 and (by, ax) in inverse or (ay, bx) not in inverse:
return abs(dy) * sy + abs(dx) * sx + "A"
else:
return abs(dx) * sx + abs(dy) * sy + "A"
def encode_shortest_numpad(code: str) -> str:
pos = "A"
res = ""
for c in code:
res += shortest_numpad(pos, c)
# print(c, res)
pos = c
return res
def encode_shortest_dirpad(code: str) -> str:
pos = "A"
res = ""
for c in code:
if pos != c:
res += shortest_dirpad(pos, c)
else:
res += "A"
pos = c
return res
def decode(code: str, pad: dict[str, tuple[int, int]]) -> str:
result = ""
inverse = {v: k for k, v in pad.items()}
y, x = pad["A"]
for i, c in enumerate(code):
match c:
case "A":
result += inverse[y, x]
case "^":
y -= 1
case "v":
y += 1
case "<":
x -= 1
case ">":
x += 1
if (y, x) not in inverse:
raise ValueError(
f"""Moved off the board {x, y}, after processing {c}.
Path so far: {result} (from {code[:i]})"""
)
return result
def count_steps(path: str, count: int) -> dict[str, int]:
cur = "A"
counts = defaultdict(int)
for c in path:
step = shortest_dirpad(cur, c)
cur = c
counts[step] += count
return counts
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
result = 0
for code in input.strip().split("\n"):
numpad = encode_shortest_numpad(code)
robot1 = encode_shortest_dirpad(numpad)
robot2 = encode_shortest_dirpad(robot1)
result += int(code[:-1]) * len(robot2)
return result
@classmethod
def part2(cls, input: str, robots=25) -> int:
result = 0
for code in input.strip().split("\n"):
numpad = encode_shortest_numpad(code)
keypresses = Counter([numpad])
for _ in range(robots + 1):
new_presses = Counter()
for subroute, count in keypresses.items():
new_presses.update(count_steps(subroute, count))
keypresses = new_presses
result += int(code[:-1]) * keypresses.total()
return result

133
2024/src/aoc/days/day24.py Normal file
View File

@@ -0,0 +1,133 @@
import functools
import re
from . import SeparateRunner
def parse_input(input: str) -> tuple[dict[str, int], dict[str, tuple[str, str, str]]]:
variable_part, rules_part = input.strip().split("\n\n")
variables = {}
for line in variable_part.splitlines():
variable, value = line.split(": ")
variables[variable] = int(value)
rules = {}
for first, op, second, result in re.findall(
r"(\w+) (XOR|OR|AND) (\w+) -> (\w+)", rules_part
):
rules[result] = (first, op, second)
return variables, rules
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
variables, rules = parse_input(input)
@functools.cache
def get_value(variable: str) -> int:
if variable in variables:
return variables[variable]
first, op, second = rules[variable]
first_v = get_value(first)
second_v = get_value(second)
match op:
case "AND":
return first_v & second_v
case "OR":
return first_v | second_v
case "XOR":
return first_v ^ second_v
result = 0
for variable in reversed(sorted(rules)):
if not variable.startswith("z"):
continue
result = result * 2 + get_value(variable)
return result
@classmethod
def part2(cls, input: str) -> str:
variables, rules = parse_input(input)
max_bit = int(
max(variable for variable in rules if variable.startswith("z"))[1:]
)
def find_invalid(output: str, pattern) -> set[str]:
if pattern is None:
return set()
if output in rules:
left, op, right = rules[output]
elif output == pattern:
return set()
else:
return {output}
pop, pleft, pright = pattern
if op != pop:
return {output}
wrong_normal = find_invalid(left, pleft) | find_invalid(right, pright)
wrong_mirror = find_invalid(left, pright) | find_invalid(right, pleft)
least_wrong = min(wrong_mirror, wrong_normal, key=len)
return least_wrong
assert max_bit >= 3
# First one is a half adder, that's a simple pattern
invalid = find_invalid("z00", ["XOR", "x00", "y00"])
# Second one is missing a reference to the before-previous adder, so it's a
# slightly different patterns
invalid |= find_invalid(
"z01", ["XOR", ["AND", "x00", "y00"], ["XOR", "x01", "y01"]]
)
# Needed a second lookback to determine that `ktp` is valid
invalid |= find_invalid(
"z02",
[
"XOR",
["XOR", "x02", "y02"],
[
"OR",
["AND", "x01", "y01"],
["AND", ["XOR", "x01", "y01"], ["AND", "x00", "y00"]],
],
],
)
for n in range(3, max_bit):
xcurr = f"x{n:02}"
ycurr = f"y{n:02}"
zcurr = f"z{n:02}"
xprev = f"x{n-1:02}"
yprev = f"y{n-1:02}"
invalid |= find_invalid(
zcurr,
[
"XOR",
["XOR", xcurr, ycurr],
[
"OR",
["AND", xprev, yprev],
["AND", ["XOR", xprev, yprev], ["OR", None, None]],
],
],
)
# This code somehow believes `ktp` is invalid, but it's fine on closer
# inspection. Will figure that out later.
return ",".join(sorted(invalid))

View File

@@ -0,0 +1,29 @@
import numpy
from . import CombinedRunner
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, None]:
blocks = input.strip().split("\n\n")
keys = []
locks = []
for block in blocks:
grid = numpy.array(list(map(list, block.splitlines())))
heights = numpy.count_nonzero(grid == "#", axis=0)
if block.startswith("#####"):
locks.append(heights)
else:
keys.append(heights)
locks = numpy.stack(locks, axis=0)
fitting = sum(
numpy.count_nonzero(numpy.all((key + locks) <= 7, axis=1)) for key in keys
)
return fitting, None

View File

@@ -0,0 +1,5 @@
029A
980A
179A
456A
379A

47
2024/tests/samples/24.txt Normal file
View File

@@ -0,0 +1,47 @@
x00: 1
x01: 0
x02: 1
x03: 1
x04: 0
y00: 1
y01: 1
y02: 1
y03: 1
y04: 1
ntg XOR fgs -> mjb
y02 OR x01 -> tnw
kwq OR kpj -> z05
x00 OR x03 -> fst
tgd XOR rvg -> z01
vdt OR tnw -> bfw
bfw AND frj -> z10
ffh OR nrd -> bqk
y00 AND y03 -> djm
y03 OR y00 -> psh
bqk OR frj -> z08
tnw OR fst -> frj
gnj AND tgd -> z11
bfw XOR mjb -> z00
x03 OR x00 -> vdt
gnj AND wpb -> z02
x04 AND y00 -> kjc
djm OR pbm -> qhw
nrd AND vdt -> hwm
kjc AND fst -> rvg
y04 OR y02 -> fgs
y01 AND x02 -> pbm
ntg OR kjc -> kwq
psh XOR fgs -> tgd
qhw XOR tgd -> z09
pbm OR djm -> kpj
x03 XOR y03 -> ffh
x00 XOR y04 -> ntg
bfw OR bqk -> z06
nrd XOR fgs -> wpb
frj XOR qhw -> z04
bqk OR frj -> z07
y03 OR x01 -> nrd
hwm AND bqk -> z03
tgd XOR rvg -> z12
tnw OR pbm -> gnj

39
2024/tests/samples/25.txt Normal file
View File

@@ -0,0 +1,39 @@
#####
.####
.####
.####
.#.#.
.#...
.....
#####
##.##
.#.##
...##
...#.
...#.
.....
.....
#....
#....
#...#
#.#.#
#.###
#####
.....
.....
#.#..
###..
###.#
###.#
#####
.....
.....
.....
#....
#.#..
#.#.#
#####

55
2024/tests/test_day21.py Normal file
View File

@@ -0,0 +1,55 @@
import pytest
from aoc.days.day21 import (
DayRunner,
encode_shortest_dirpad,
encode_shortest_numpad,
)
from . import get_data
def test_encode_shortest_numpad() -> None:
assert encode_shortest_numpad("029A") in (
"<A^A>^^AvvvA",
"<A^A^>^AvvvA",
"<A^A^^>AvvvA",
)
def test_encode_shortest_dirpad() -> None:
numpad_encoded = encode_shortest_numpad("029A")
assert len(encode_shortest_dirpad(numpad_encoded)) == len(
"v<<A>>^A<A>AvA<^AA>A<vAAA>^A"
)
@pytest.mark.parametrize(
"code,answer",
[
(
"029A",
"<vA<AA>>^AvAA<^A>A<v<A>>^AvA^A<vA>^A<v<A>^A>AAvA^A<v<A>A>^AAAvA<^A>A",
),
("980A", "<v<A>>^AAAvA^A<vA<AA>>^AvAA<^A>A<v<A>A>^AAAvA<^A>A<vA>^A<A>A"),
(
"179A",
"<v<A>>^A<vA<A>>^AAvAA<^A>A<v<A>>^AAvA^A<vA>^AA<A>A<v<A>A>^AAAvA<^A>A",
),
("456A", "<v<A>>^AA<vA<A>>^AAvAA<^A>A<vA>^A<A>A<vA>^A<A>A<v<A>A>^AAvA<^A>A"),
("379A", "<v<A>>^AvA^A<vA<AA>>^AAvA<^A>AAvA^A<vA>^AA<A>A<v<A>A>^AAAvA<^A>A"),
],
)
def test_encode_shortest_dirpad_twice(code: str, answer: str) -> None:
numpad_encoded = encode_shortest_numpad(code)
robot1 = encode_shortest_dirpad(numpad_encoded)
robot2 = encode_shortest_dirpad(robot1)
assert len(robot2) == len(answer)
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(21)) == 126384
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(21), robots=2) == 126384

7
2024/tests/test_day24.py Normal file
View File

@@ -0,0 +1,7 @@
from aoc.days.day24 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(24)) == 2024

7
2024/tests/test_day25.py Normal file
View File

@@ -0,0 +1,7 @@
from aoc.days.day25 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(25)) == 3

5
2025/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Advent of Code 2025
New year, new challenge. With the shorter year, I'm going to try to use a different language every
day, restricting myself to programming languages my employer uses in production. Let's call it the
**Full Stack Challenge**.

17
2025/day01/README.md Normal file
View File

@@ -0,0 +1,17 @@
# Day 01: Nix
Nix is a functional language made for the Nix package manager. As
To run the solution program, start the `nix` repl with the solution program and call the `solve`
function with the path to the input file.
```console
$ nix repl --option max-call-depth 10000 --file solve.nix
nix-repl> solve ./sample.txt
```
Some observations:
- The `max-call-depth` needs to be bumped to at least 10k for the main input files, otherwise you
hit the limit in the recursion.
- The standard library is lacking several basics but most of those you can build yourself

10
2025/day01/sample.txt Normal file
View File

@@ -0,0 +1,10 @@
L68
L30
R48
L5
R60
L55
L1
L99
R14
L82

37
2025/day01/solve.nix Normal file
View File

@@ -0,0 +1,37 @@
{
solve = input_file:
let
data = builtins.readFile input_file;
lines = builtins.filter (s: s != "" && builtins.isString s) (builtins.split "\n" data);
# Basic maths because the standard library doesn't have it
mod = a: b: a - (a / b) * b;
abs = n: if n < 0 then -n else n;
recurse = list: position: score1: score2:
let
first = builtins.head list;
# This is not guaranteed to work but it's good enough
num_part = builtins.substring 1 999 first;
num = builtins.fromJSON num_part;
delta = mod (if (builtins.substring 0 1 first) == "R" then num else -num) 100;
next = mod (position + delta + 100) 100;
score = if next == 0 then 1 else 0;
circles = num / 100;
passed = if position == 0 then
0
else
if delta < 0 && delta + position <= 0 || delta > 0 && delta + position >= 100 then
1
else
0;
in
if list == [] then
[score1 score2]
else
recurse (builtins.tail list) next (score + score1) (score2 + passed + circles);
in
recurse lines 50 0 0;
}

29
2025/day02/README.md Normal file
View File

@@ -0,0 +1,29 @@
# Day 02: Terraform
The code assumes an input file at `../inputs/02.txt`. Other than that, simply try to run as follows:
```console
$ terraform init
Initializing the backend...
Initializing modules...
Initializing provider plugins...
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
$ terraform plan
….
Changes to Outputs:
+ part1 = secret
+ part2 = also secret
You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
```

29
2025/day02/main.tf Normal file
View File

@@ -0,0 +1,29 @@
locals {
input = file("../inputs/02.txt")
ranges = split(",", chomp(local.input))
min_max = [for r in local.ranges : split("-", r)]
}
module "check_range" {
source = "./range"
count = length(local.min_max)
min = local.min_max[count.index][0]
max = local.min_max[count.index][1]
}
module "check_range2" {
source = "./range2"
count = length(local.min_max)
min = local.min_max[count.index][0]
max = local.min_max[count.index][1]
}
output "part1" {
value = sum(module.check_range[*].invalid_sum)
}
output "part2" {
value = sum(module.check_range2[*].invalid_sum)
}

View File

@@ -0,0 +1,15 @@
variable "part" {
type = number
}
variable "repetitions" {
type = number
}
locals {
repeated = [for _ in range(var.repetitions) : tostring(var.part)]
}
output "full" {
value = join("", local.repeated)
}

46
2025/day02/range/main.tf Normal file
View File

@@ -0,0 +1,46 @@
variable "min" {
type = number
}
variable "max" {
type = number
}
variable "repetitions" {
type = number
default = 2
}
locals {
digits = floor(length(tostring(var.max)) / var.repetitions)
maximum = substr(tostring(var.max), 0, local.digits)
real_maximum = length(tostring(var.max)) % var.repetitions == 0 ? tonumber(local.maximum) : pow(10, local.digits)
minimum_prefix = substr(tostring(var.min), 0, length(tostring(var.min)) - local.digits * (var.repetitions - 1))
minimum = local.minimum_prefix == "" ? 1 : tonumber(local.minimum_prefix)
count = max(local.real_maximum - local.minimum + 1, 1)
can_work = anytrue([for n in range(length(tostring(var.min)), length(tostring(var.max)) + 1) : n % var.repetitions == 0])
}
// This "candidates" module ought really be a list comprehension from range, but Terraform does not
// allow you to create ranges longer than 1024.
module "candidates" {
source = "./item"
count = local.can_work ? local.count : 0
part = count.index + local.minimum
repetitions = var.repetitions
}
locals {
invalid = [for n in module.candidates[*].full : n if n >= var.min && n <= var.max]
}
output "invalid_sum" {
value = length(local.invalid) > 0 ? sum(local.invalid) : 0
}
output "invalid" {
value = toset(local.invalid)
}

27
2025/day02/range2/main.tf Normal file
View File

@@ -0,0 +1,27 @@
variable "min" {
type = number
}
variable "max" {
type = number
}
locals {
digits = length(tostring(var.max))
}
module "range" {
source = "../range"
count = local.digits - 1
max = var.max
min = var.min
repetitions = count.index + 2
}
locals {
results = setunion(module.range[*].invalid...)
}
output "invalid_sum" {
value = length(local.results) > 0 ? sum(local.results) : 0
}

128
2025/day02/tests.tftest.hcl Normal file
View File

@@ -0,0 +1,128 @@
run "sample-1-1" {
module {
source = "./range"
}
variables {
min = 11
max = 22
}
assert {
condition = output.invalid_sum == 33
error_message = "Incorrect result"
}
}
run "sample-1-2" {
module {
source = "./range"
}
variables {
min = 95
max = 115
}
assert {
condition = output.invalid_sum == 99
error_message = "Incorrect result"
}
}
run "sample-2-1" {
module {
source = "./range2"
}
variables {
min = 11
max = 22
}
assert {
condition = output.invalid_sum == 33
error_message = "Incorrect result"
}
}
run "sample-2-2" {
module {
source = "./range2"
}
variables {
min = 95
max = 115
}
assert {
condition = output.invalid_sum == 210
error_message = "Incorrect result"
}
}
run "sample-2-3" {
module {
source = "./range2"
}
variables {
min = 998
max = 1012
}
assert {
condition = output.invalid_sum == 2009
error_message = "Incorrect result"
}
}
run "sample-2-2-detail" {
module {
source = "./range"
}
variables {
min = 95
max = 115
repetitions = 3
}
assert {
condition = output.invalid_sum == 111
error_message = "Incorrect result"
}
}
run "sample-2-10" {
module {
source = "./range2"
}
variables {
min = 824824821
max = 824824827
}
assert {
condition = output.invalid_sum == 824824824
error_message = "Incorrect result"
}
}
run "sample-2-11" {
module {
source = "./range2"
}
variables {
min = 2121212118
max = 2121212124
}
assert {
condition = output.invalid_sum == 2121212121
error_message = "Incorrect result"
}
}

4
2025/day03/README.md Normal file
View File

@@ -0,0 +1,4 @@
# Day 03: Bash
Simple as can be, simply run `./solve.sh <input file>` and get both answers. While I allowed myself
to use `coreutils` with bash, they turned out to be entirely unnecessary. Pure bash just works.

4
2025/day03/sample.txt Normal file
View File

@@ -0,0 +1,4 @@
987654321111111
811111111111119
234234234234278
818181911112111

64
2025/day03/solve.sh Executable file
View File

@@ -0,0 +1,64 @@
#!/bin/bash
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <input file>" >&2
exit 1
fi
joltage() {
local first second c i
first=0
second=0
for ((i = 0; i < ${#1}; i++)); do
c="${1:$i:1}"
if [[ $second -gt $first ]]; then
first="$second"
second="$c"
elif [[ $c -gt $second ]]; then
second="$c"
fi
done
echo "$first$second"
}
joltage2() {
local c i d e n digits
digits=(0 0 0 0 0 0 0 0 0 0 0 0)
for ((i = 0; i < ${#1}; i++)); do
c="${1:$i:1}"
for ((d = 0; d < 11; d++)); do
if [[ ${digits[((d + 1))]} -gt ${digits[$d]} ]]; then
for ((e = d; e < 11; e++)); do
n=$((e + 1))
digits[e]=${digits[n]}
done
digits[11]="0"
break
fi
done
if [[ $c -gt ${digits[11]} ]]; then
digits[11]="$c"
fi
done
printf "%s" "${digits[@]}"
}
total=0
total2=0
while IFS="" read -r line || [[ -n "$p" ]]; do
((total += $(joltage "$line")))
((total2 += $(joltage2 "$line")))
done <"$1"
echo "$total"
echo "$total2"

11
2025/day04/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Day 04: SQLite sql
Did you know SQLite does not have procedural extensions? I did not when I started this. Regardless,
to run:
```console
$ sqlite3 < solve.sql
part1
-----
13
```

10
2025/day04/sample.txt Normal file
View File

@@ -0,0 +1,10 @@
..@@.@@@@.
@@@.@.@.@@
@@@@@.@.@@
@.@@@@..@.
@@.@@@@.@@
.@@@@@@@.@
.@.@.@.@@@
@.@@@.@@@@
.@@@@@@@@.
@.@.@@@.@.

759
2025/day04/solve.sql Normal file
View File

@@ -0,0 +1,759 @@
-- First import raw data into a table so we can work
CREATE TABLE raw_data(line_data VARCHAR NOT NULL);
.import --csv './sample.txt' raw_data
-- Then use the auto-incrementing ID to add a y coordinate
CREATE TABLE grid_lines(
y INTEGER PRIMARY KEY,
line_data VARCHAR NOT NULL
);
INSERT INTO grid_lines(line_data)
SELECT line_data
FROM raw_data;
-- Now create a table to hold the paper rolls
CREATE TABLE rolls(
x INTEGER NOT NULL,
y INTEGER NOT NULL,
PRIMARY KEY (x, y)
);
WITH RECURSIVE cte AS (
SELECT y,
1 x,
line_data,
substr(line_data, 1, 1) c
FROM grid_lines
UNION ALL
SELECT y,
x + 1,
line_data,
substr(line_data, x + 1, 1)
FROM cte
WHERE x <= length(line_data)
)
INSERT INTO rolls
SELECT x,
y
FROM cte
WHERE c = '@';
-- Now compute part 1
SELECT COUNT(*) as part1
FROM rolls r
WHERE (
SELECT COUNT(*) - 1
FROM rolls o
WHERE o.x >= r.x - 1
AND o.x <= r.x + 1
AND o.y >= r.y - 1
AND o.y <= r.y + 1
) < 4;
--- Create a scratch table where we're going to delete rolls from
CREATE TABLE rolls2(
x INTEGER NOT NULL,
y INTEGER NOT NULL,
PRIMARY KEY (x, y)
);
INSERT INTO rolls2
SELECT *
FROM rolls;
-- Delete the outer rolls of paper 77 times. This turns out to be enough.
-- I tried really hard not to do this.
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
DELETE FROM rolls2
WHERE (
SELECT COUNT(*) - 1
FROM rolls2 o
WHERE o.x >= rolls2.x - 1
AND o.x <= rolls2.x + 1
AND o.y >= rolls2.y - 1
AND o.y <= rolls2.y + 1
) < 4;
-- See how many we've deleted
SELECT (
SELECT COUNT(*)
FROM rolls
) - (
SELECT COUNT(*)
FROM rolls2
) as part2;

1
2025/day05/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
solve

6
2025/day05/Makefile Normal file
View File

@@ -0,0 +1,6 @@
CFLAGS=-Wall -Wextra -O2 -g
all: solve
clean:
$(RM) solve

12
2025/day05/README.md Normal file
View File

@@ -0,0 +1,12 @@
# Day 05: C
C is weird. It's a very simple language but it's made complicated by the fact that you have to do
everything yourself. Relatively straightforward solution.
```console
$ make
cc -Wall -Wextra -O2 -g solve.c -o solve
$ ./solve sample.txt
Part1: 3
Part2: 14
```

11
2025/day05/sample.txt Normal file
View File

@@ -0,0 +1,11 @@
3-5
10-14
16-20
12-18
1
5
8
11
17
32

160
2025/day05/solve.c Normal file
View File

@@ -0,0 +1,160 @@
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#define MAX(a,b) \
({ __typeof__ (a) _a = (a); \
__typeof__ (b) _b = (b); \
_a > _b ? _a : _b; })
typedef struct {
uint64_t start;
uint64_t end;
} range_t;
typedef struct {
size_t length;
size_t capacity;
range_t* buffer;
} ranges_t;
static int cmp_range(void const* va, void const* vb) {
range_t const* a = va;
range_t const* b = vb;
if (a->start == b->start) {
return (a->end > b->end) - (a->end < b->end);
} else {
return (a->start > b->start) - (a->start < b-> start);
}
}
static void simplify_ranges(ranges_t* ranges) {
// First, sort ranges ascending by start (then end)
qsort(ranges->buffer, ranges->length, sizeof(range_t), cmp_range);
const size_t length = ranges->length;
range_t* write_ptr = &ranges->buffer[0];
// Important, skip the first iteration, it can never be merged
for (size_t i = 1; i < length; ++i) {
if (ranges->buffer[i].start <= write_ptr->end) {
// Merge the two ranges
write_ptr->end = MAX(write_ptr->end, ranges->buffer[i].end);
ranges->length--;
} else {
write_ptr++;
if (write_ptr != &ranges->buffer[i]) {
memcpy(write_ptr, &ranges->buffer[i], sizeof(range_t));
}
}
}
// for (size_t i = 0; i < ranges->length; ++i) {
// printf("%" PRIu64 "-%" PRIu64 "\n", ranges->buffer[i].start, ranges->buffer[i].end);
// }
// printf("Original length: %u, new length: %u\n", (unsigned int) length, (unsigned int) ranges->length);
}
bool is_fresh(const uint64_t ingredient, ranges_t const * const ranges) {
size_t min = 0;
size_t size = ranges->length;
range_t const* buffer = ranges->buffer;
while (size > 1) {
size_t half = size / 2;
size_t mid = min + half;
if (buffer[mid].start <= ingredient) {
min = mid;
}
size -= half;
}
return buffer[min].start <= ingredient && buffer[min].end >= ingredient;
}
int main(const int argc, char const** argv) {
if (argc < 2) {
printf("Usage: %s <input>\n", argv[0]);
return 1;
}
ranges_t ranges = {
.length = 0,
.capacity = 2,
.buffer = malloc(2 * sizeof(range_t)),
};
FILE* input = fopen(argv[1], "r");
if (input == NULL) {
perror("failed to open file");
exit(1);
}
uint64_t ingredient;
bool seen_first = false;
while (!seen_first) {
uint64_t start;
uint64_t end;
switch (fscanf(input, "%" PRIu64 "-%" PRIu64, &start, &end)) {
case 1:
seen_first = true;
ingredient = start;
break;
case 2:
if (ranges.capacity == ranges.length) {
size_t new_capacity = ranges.capacity * 2;
range_t* buffer = realloc(ranges.buffer, new_capacity * sizeof(range_t));
if (buffer == NULL) {
printf("Failed to resize ranges buffer\n");
return 1;
}
ranges.capacity = new_capacity;
ranges.buffer = buffer;
}
ranges.buffer[ranges.length].start = start;
ranges.buffer[ranges.length].end = end;
ranges.length++;
break;
default:
printf("Unexpected end of file\n");
return 1;
}
}
simplify_ranges(&ranges);
int total_fresh = 0;
do {
if (is_fresh(ingredient, &ranges)) {
total_fresh++;
}
} while (fscanf(input, "%" PRIu64 "\n", &ingredient) == 1);
printf("Part1: %u\n", total_fresh);
uint64_t combined_fresh = 0;
for (size_t i = 0; i < ranges.length; ++i) {
combined_fresh += ranges.buffer[i].end - ranges.buffer[i].start + 1;
}
printf("Part2: %" PRIu64 "\n", combined_fresh);
fclose(input);
free(ranges.buffer);
return 0;
}

10
2025/day06/README.md Normal file
View File

@@ -0,0 +1,10 @@
# Day 06: PHP
Simple and straight-forward. This is a normal programming language.
```console
$ ./solve.php sample.txt
Part1: 4277556
Part2: 3263827
```

4
2025/day06/sample.txt Normal file
View File

@@ -0,0 +1,4 @@
123 328 51 64
45 64 387 23
6 98 215 314
* + * +

81
2025/day06/solve.php Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env php
<?php
if ($argc < 2) {
echo "Usage: {$argv[0]} INPUT\n";
exit(1);
}
$input = file($argv[1]);
function part1(string ...$input): int {
$lines = [];
foreach ($input as $line) {
$lines[] = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY);
}
$lines = array_reverse($lines);
$cols = count($lines[0]);
$rows = count($lines);
$sum = 0;
for ($col = 0; $col < $cols; ++$col) {
if ($lines[0][$col] === '+') {
$acc = 0;
for ($row = 1; $row < $rows; ++$row) {
$acc += (int) $lines[$row][$col];
}
} else {
$acc = 1;
for ($row = 1; $row < $rows; ++$row) {
$acc *= (int) $lines[$row][$col];
}
}
$sum += $acc;
}
return $sum;
}
function part2(string ...$lines): int {
$cols = strlen($lines[0]);
$rows = count($lines);
$numbers = array_fill(0, $cols, "");
for ($row = 0; $row < $rows - 1; ++$row) {
foreach (str_split($lines[$row]) as $col => $c) {
if ($c !== ' ') {
$numbers[$col] .= $c;
}
}
}
$sum = 0;
foreach (str_split($lines[$rows - 1]) as $col => $c) {
switch ($c) {
case '+':
$acc = 0;
for ($i = $col; $i < $cols && $numbers[$i] !== ""; ++$i) {
$acc += (int) $numbers[$i];
}
$sum += $acc;
break;
case '*':
$acc = 1;
for ($i = $col; $i < $cols && $numbers[$i] !== ""; ++$i) {
$acc *= (int) $numbers[$i];
}
$sum += $acc;
break;
}
}
return $sum;
}
echo "Part1: " . part1(...$input) . "\n";
echo "Part2: " . part2(...$input) . "\n";

3
2025/day07/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
solve
*.o
*.hi

14
2025/day07/Makefile Normal file
View File

@@ -0,0 +1,14 @@
.PHONY: all clean test
all: solve
test: solve
./solve sample.txt
solve: solve.hs
ghc -dynamic -O -g -o $@ $^
clean:
$(RM) solve
$(RM) *.o
$(RM) *.hi

15
2025/day07/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Day 07: Haskell
This one took me the longest, mostly because I really do not know Haskell. Nevertheless, I find the
resulting solution quite elegant. The `Makefile` I made uses dynamic linkage as that's how Haskell
works on Arch Linux. If you want to do static linking, `ghc solve.hs` should work.
```console
$ make
ghc -dynamic -O -g -o solve solve.hs
[1 of 2] Compiling Main ( solve.hs, solve.o )
[2 of 2] Linking solve
$ ./solve sample.txt
21
40
```

16
2025/day07/sample.txt Normal file
View File

@@ -0,0 +1,16 @@
.......S.......
...............
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............

65
2025/day07/solve.hs Normal file
View File

@@ -0,0 +1,65 @@
import Data.Array
import Data.List
import Data.Maybe
import System.Environment
import System.Exit
main = getArgs >>= parse >>= run
stringToArray :: [Char] -> Array Int Char
stringToArray s = listArray (0, length s - 1) s
run :: String -> IO ()
run input =
let gridLines = lines input
startingPoint = findStartingPoint $ head gridLines
arrLines = Data.List.map stringToArray $ tail gridLines
in do
print $ part1 arrLines startingPoint
print $ part2 arrLines startingPoint
findStartingPoint :: [Char] -> Int
findStartingPoint line = fromJust $ elemIndex 'S' line
part1 arrLines startingPoint =
let initial = [startingPoint]
in simulate arrLines 0 initial
checkHit :: Array Int Char -> Int -> [Int]
checkHit line idx = case line ! idx of
'^' -> (idx - 1) : [idx + 1]
_ -> [idx]
simulate [] count active = count
simulate lines count active =
let arr = head lines
followUp = nub $ concatMap (checkHit arr) active
hits = length $ Data.List.filter (\i -> arr ! i == '^') active
remainder = tail lines
in simulate remainder (hits + count) followUp
part2 :: [Array Int Char] -> Int -> Int
part2 arrLines startingPosition =
let n = length arrLines
arr = listArray (0, n - 1) arrLines
width = length $ head arrLines
compute i pos
| i >= n = 1
| otherwise =
let line = arr ! i
in case line ! pos of
'^' -> memo ! (i + 1, pos - 1) + memo ! (i + 1, pos + 1)
_ -> memo ! (i + 1, pos)
memo =
array
((0, 0), (n, width - 1))
[((i, pos), compute i pos) | i <- [0 .. n], pos <- [0 .. width - 1]]
in memo ! (0, startingPosition)
parse ["-h"] = usage >> exitSuccess
parse [] = usage >> die usageStr
parse fs = concat `fmap` mapM readFile fs
usageStr = "Usage: solve [-vh] [file ..]"
usage = putStrLn usageStr

1
2025/day08/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
solve

11
2025/day08/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Day 08: Go
Nice puzzle, got to use an obscure data structure. And implement it, because Go doesn't have it in
its standard library.
```console
$ go build
$ ./solve sample.txt 10
Part 1: 40
Part 2: 25272
```

3
2025/day08/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module bertptrs/solve/v2
go 1.25.5

20
2025/day08/sample.txt Normal file
View File

@@ -0,0 +1,20 @@
162,817,812
57,618,57
906,360,560
592,479,940
352,342,300
466,668,158
542,29,236
431,825,988
739,650,466
52,470,668
216,146,977
819,987,18
117,168,530
805,96,715
346,949,466
970,615,88
941,993,340
862,61,35
984,92,344
425,690,689

192
2025/day08/solve.go Normal file
View File

@@ -0,0 +1,192 @@
package main
import (
"bufio"
"container/heap"
"fmt"
"os"
"sort"
"strconv"
)
type SetEntry struct {
parent, size int
}
type DisjointSet struct {
set []SetEntry
}
func NewDisjointSet(size int) *DisjointSet {
set := make([]SetEntry, size)
for i := range len(set) {
set[i].parent = i
set[i].size = 1
}
instance := new(DisjointSet)
instance.set = set
return instance
}
func (d *DisjointSet) Find(item int) int {
for d.set[item].parent != item {
d.set[item].parent = d.set[d.set[item].parent].parent
item = d.set[item].parent
}
return item
}
func (d *DisjointSet) Union(x, y int) bool {
xp := d.Find(x)
yp := d.Find(y)
if xp == yp {
return false
}
if yp < xp {
xp, yp = yp, xp
}
d.set[xp].size += d.set[yp].size
d.set[yp].parent = xp
return true
}
func (d *DisjointSet) Size(item int) int {
if d.set[item].parent == item {
return d.set[item].size
} else {
return 0
}
}
type DistanceHeap [][3]int
func (h DistanceHeap) Len() int { return len(h) }
func (h DistanceHeap) Less(i, j int) bool { return h[i][0] < h[j][0] }
func (h DistanceHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *DistanceHeap) Push(x any) {
// Push and Pop use pointer receivers because they modify the slice's length,
// not just its contents.
*h = append(*h, x.([3]int))
}
func (h *DistanceHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
func read_input(filename string) [][3]int {
var points [][3]int
var point [3]int
file, err := os.Open(filename)
if err != nil {
panic(err)
}
defer file.Close()
reader := bufio.NewReader(file)
for {
parsed, err := fmt.Fscanf(reader, "%d,%d,%d\n", &point[0], &point[1], &point[2])
if err != nil || parsed != 3 {
break
}
points = append(points, point)
}
return points
}
func usage() {
fmt.Printf("Usage: %v INPUT_FILE [connections]\n", os.Args[0])
os.Exit(1)
}
func compute_group_sizes(groups *DisjointSet, len int) []int {
var sizes []int
for i := range len {
size := groups.Size(i)
if size > 0 {
sizes = append(sizes, size)
}
}
sort.Ints(sizes)
return sizes
}
func main() {
if len(os.Args) < 2 {
usage()
}
connections := 1000
if len(os.Args) >= 3 {
parsed, err := strconv.Atoi(os.Args[2])
if err != nil {
usage()
}
connections = parsed
}
points := read_input(os.Args[1])
distances := make([][3]int, 0, len(points)*(len(points)-1)/2)
for i, a := range points {
for j := i + 1; j < len(points); j += 1 {
b := points[j]
square_dist := (a[0]-b[0])*(a[0]-b[0]) + (a[1]-b[1])*(a[1]-b[1]) + (a[2]-b[2])*(a[2]-b[2])
distances = append(distances, [3]int{square_dist, i, j})
}
}
size_heap := DistanceHeap(distances)
heap.Init(&size_heap)
groups := NewDisjointSet(len(points))
for range connections {
first := heap.Pop(&size_heap)
d := first.([3]int)
groups.Union(d[1], d[2])
}
sizes := compute_group_sizes(groups, len(points))
product := 1
for _, size := range sizes[len(sizes)-3:] {
product *= size
}
fmt.Printf("Part 1: %v\n", product)
to_merge := len(sizes) - 1
for {
first := heap.Pop(&size_heap)
d := first.([3]int)
if groups.Union(d[1], d[2]) {
to_merge -= 1
if to_merge == 0 {
fmt.Printf("Part 2: %v\n", points[d[1]][0]*points[d[2]][0])
return
}
}
}
}

1
2025/day09/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

14
2025/day09/README.md Normal file
View File

@@ -0,0 +1,14 @@
# Day 9: Typescript
Lost a lot of time on a swapped argument order. Oh well, such is life. The `package.json` exists to
instruct the interpreter on how to execute the file and doesn't otherwise include any meaningful
dependencies. Everything works with the standard library for Node.
`ts-node` is used for just-in-time Typescript compilation. You can also compile the file manually
first, then run it as JS. Also, Oracle, please release the name Javascript.
```console
$ ./solve.ts sample.txt
Part 1: 50
Part 2: 24
```

29
2025/day09/package-lock.json generated Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "day09",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"@types/node": "^24.10.2"
}
},
"node_modules/@types/node": {
"version": "24.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.2.tgz",
"integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
}
}
}

6
2025/day09/package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"type": "module",
"devDependencies": {
"@types/node": "^24.10.2"
}
}

8
2025/day09/sample.txt Normal file
View File

@@ -0,0 +1,8 @@
7,1
11,1
11,7
9,7
9,5
2,5
2,3
7,3

78
2025/day09/solve.ts Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env ts-node
import * as fs from 'fs';
import { exit } from 'process';
if (process.argv.length < 3) {
console.log("Usage: " + process.argv0 + " " + process.argv[1] + " INPUT_FILE");
exit(10);
}
const input_file = fs.readFileSync(process.argv[2], "utf-8");
const lines = input_file.trim().split("\n");
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const points = lines.map(line => {
const [x, y] = line.split(",");
return new Point(+x, +y);
});
let max_size = 0;
let max_size_contained = 0;
function has_intersection(left: number, right: number, bottom: number, top: number): boolean {
for (let i = 0; i < points.length; ++i) {
const first = points[i];
const second = points[(i + 1) % points.length];
if (first.x == second.x) {
const yMin = Math.min(first.y, second.y);
const yMax = Math.max(first.y, second.y);
if (left < first.x && first.x < right && (yMin <= bottom && bottom < yMax || yMin < top && top <= yMax)) {
return true;
}
} else if (first.y == second.y) {
const xMin = Math.min(first.x, second.x);
const xMax = Math.max(first.x, second.x);
if (bottom < first.y && first.y < top && (xMin <= left && left < xMax || xMin < right && right <= xMax)) {
return true;
}
} else {
throw "Invalid input";
}
}
return false;
}
for (let i = 0; i < points.length; ++i) {
for (let j = i + 1; j < points.length; ++j) {
const left = Math.min(points[i].x, points[j].x);
const right = Math.max(points[i].x, points[j].x);
const bottom = Math.min(points[i].y, points[j].y);
const top = Math.max(points[i].y, points[j].y);
const width = right - left + 1;
const height = top - bottom + 1;
const area = width * height;
max_size = Math.max(max_size, area);
if (area > max_size_contained && !has_intersection(left, right, bottom, top)) {
max_size_contained = area;
}
}
}
console.log("Part 1:", max_size);
// Too high: 4531758980
console.log("Part 2:", max_size_contained);

1
2025/day10/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target/

7
2025/day10/Cargo.toml Normal file
View File

@@ -0,0 +1,7 @@
[package]
name = "solve"
version = "0.1.0"
edition = "2024"
[dependencies]
microlp = "0.2.11"

3
2025/day10/sample.txt Normal file
View File

@@ -0,0 +1,3 @@
[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
[...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}
[.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}

141
2025/day10/src/main.rs Normal file
View File

@@ -0,0 +1,141 @@
use std::collections::VecDeque;
use std::env;
use std::fs;
use std::io;
use microlp::ComparisonOp;
use microlp::LinearExpr;
use microlp::OptimizationDirection;
use microlp::Problem;
fn parse_line(line: &str) -> (u32, Vec<u32>, Vec<u8>) {
let mut buttons = vec![];
let mut target = 0;
let mut it = line.chars();
for (i, c) in it.by_ref().skip(1).enumerate() {
match c {
'#' => target |= 1 << i,
'.' => (),
_ => break,
}
}
loop {
match it.nth(1) {
Some('{') => break,
Some('(') => (),
other => panic!("Unexpected character \"{other:?}\" in: {line}"),
}
let mut button = 0;
while let Some(c) = it.next() {
let d = c.to_digit(10).unwrap();
button |= 1 << d;
if let Some(')') = it.next() {
break;
}
}
buttons.push(button);
}
let rem = it.as_str().trim().trim_end_matches('}');
let joltage = rem.split(',').map(|j| j.parse().unwrap()).collect();
(target, buttons, joltage)
}
fn min_joltage(buttons: &[u32], joltage: &[u8]) -> i32 {
let mut problem = Problem::new(OptimizationDirection::Minimize);
let max = i32::from(*joltage.iter().max().unwrap_or(&0));
let variables: Vec<_> = buttons
.iter()
.map(|_| problem.add_integer_var(1.0, (0, max)))
.collect();
for (bit, &value) in joltage.iter().enumerate() {
let mut equation = LinearExpr::empty();
for (&button, &variable) in buttons.iter().zip(&variables) {
if button & (1 << bit) != 0 {
equation.add(variable, 1.0);
}
}
problem.add_constraint(equation, ComparisonOp::Eq, value.into());
}
problem.solve().unwrap().objective().round() as i32
}
fn minimum_clicks(target: u32, buttons: &[u32]) -> i32 {
let max = buttons
.iter()
.map(|s| 32 - s.leading_zeros())
.max()
.unwrap_or(0);
let possible = 2 << max;
let mut seen = vec![false; possible];
let mut todo = VecDeque::new();
todo.push_back((0, 0));
while let Some((steps, state)) = todo.pop_front() {
for &button in buttons {
let next = state ^ button;
if next == target {
return steps + 1;
} else if !seen[next as usize] {
seen[next as usize] = true;
todo.push_back((steps + 1, next));
}
}
}
unreachable!("Did not find target");
}
fn solve(input: &str) -> (i32, i32) {
let mut total_clicks = 0;
let mut total_presses = 0;
for line in input.trim().lines() {
let (target, buttons, joltage) = parse_line(line);
total_clicks += minimum_clicks(target, &buttons);
total_presses += min_joltage(&buttons, &joltage)
}
(total_clicks, total_presses)
}
fn main() -> io::Result<()> {
if let Some(path) = env::args_os().nth(1) {
let input = fs::read_to_string(path)?;
let (part1, part2) = solve(&input);
println!("Part 1: {part1}\nPart 2: {part2}");
Ok(())
} else {
eprintln!("Usage: {} INPUT_FILE", env::args().next().unwrap());
std::process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = include_str!("../sample.txt");
#[test]
fn test_sample() {
let (part1, part2) = solve(SAMPLE);
assert_eq!(7, part1);
assert_eq!(33, part2);
}
}

10
2025/day11/README.md Normal file
View File

@@ -0,0 +1,10 @@
# Day 11: Python
Straightforward. Uses `networkx` because I thought it would be very helpful but it only really saves
me from writing a topological sort algorithm in the end.
```console
$ uv run ./solve.py input.txt
Part 1: secret
Part 2: secret
```

View File

@@ -0,0 +1,9 @@
[project]
name = "solve"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"networkx>=3.6.1",
]

10
2025/day11/sample.txt Normal file
View File

@@ -0,0 +1,10 @@
aaa: you hhh
you: bbb ccc
bbb: ddd eee
ccc: ddd eee fff
ddd: ggg
eee: out
fff: out
ggg: out
hhh: ccc fff iii
iii: out

14
2025/day11/sample2.txt Normal file
View File

@@ -0,0 +1,14 @@
svr: aaa bbb
aaa: fft
fft: ccc
bbb: tty
tty: ccc
ccc: ddd eee
ddd: hub
hub: fff
eee: dac
dac: fff
fff: ggg hhh
ggg: out
hhh: out
you: out

70
2025/day11/solve.py Executable file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
from collections import defaultdict
import fileinput
from typing import Iterable
import networkx as nx
def parse_graph() -> nx.Graph:
graph = nx.DiGraph()
for line in fileinput.input():
source, rem = line.split(": ")
for sink in rem.strip().split(" "):
graph.add_edge(source, sink)
return graph
def count(gen: Iterable) -> int:
return sum(1 for _ in gen)
def main() -> None:
graph = parse_graph()
# Observation: graph is a DAG, so one needs to go in front of the other. We can do this in three
# steps:
# svr → closest(dac, fft) -> furthest(dac,fft)
rank = {
node: rank
for rank, node in enumerate(nx.topological_sort(graph))
}
rev_rank = {rank: node for node, rank in rank.items()}
if rank["dac"] > rank["fft"]:
closest = "fft"
furthest = "dac"
else:
closest = "dac"
furthest = "fft"
def ranked_all_paths(source: str, dest: str) -> int:
counts = defaultdict(int)
counts[dest] = 1
for r in range(rank[dest], rank[source], -1):
node = rev_rank[r]
if node not in counts:
continue
for u, _ in graph.in_edges(node):
counts[u] += counts[node]
return counts[source]
assert nx.has_path(graph, closest, furthest)
print("Part 1", ranked_all_paths("you", "out"))
first = ranked_all_paths("svr", closest)
second = ranked_all_paths(closest, furthest)
third = ranked_all_paths(furthest, "out")
print("Part 2:", first * second * third)
if __name__ == "__main__":
main()

0
2025/inputs/.gitkeep Normal file
View File