99 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
48824288b0 Implement 2024 day 23
Is using networkx cheating? I don't think so. I've already solved
max-clique before, so I know I can do it
2024-12-23 08:21:45 +01:00
9e552c9b6a More inlining 2024-12-23 08:00:10 +01:00
329cd4e471 More optimizations 2024-12-22 14:11:39 +01:00
321d78d850 More loops to numpy 2024-12-22 12:15:11 +01:00
230b4ae028 Implement 2024 day 22 2024-12-22 12:03:44 +01:00
be2244eca9 Inline module
Because this is Terraform, and it's the ultimate optimisation technique
2024-12-20 20:15:30 +01:00
395463dc4a Implement 2024 day 20 2024-12-20 09:46:53 +01:00
fdaadfe184 Implement day 19 part 1 in Terraform 2024-12-19 23:24:11 +01:00
dd07090056 Poor man's trie 2024-12-19 22:35:55 +01:00
891a5ea50c Inline function to avoid passing arg 2024-12-19 09:13:37 +01:00
d4aad95f55 Implement 2024 day 19 2024-12-19 09:05:55 +01:00
caa2c9b6cf Implement 2024 day 18 2024-12-18 09:14:39 +01:00
4a8b9f9109 Implement 2024 day 17 part 2 2024-12-17 22:17:12 +01:00
6c0a49a5f3 Implement 2024 day 17 part 1 2024-12-17 09:55:07 +01:00
17017e7ab4 Formatting? 2024-12-17 09:34:43 +01:00
b23f24c567 Implement 2024 day 16 part 2 2024-12-16 18:16:05 +01:00
f0556c6afe Implement 2024 day 16 part 1 2024-12-16 08:58:42 +01:00
da7ee96b93 Slihghtly simplify 2024-12-16 08:33:02 +01:00
e90ee40880 Implement 2024 day 15 2024-12-15 14:05:31 +01:00
98983f622c It looks nice, not what I expected 2024-12-14 09:18:36 +01:00
4a7305d7eb Implement 2024 day 14 2024-12-14 09:11:16 +01:00
c8ab67d145 Implement 2024 day 13 2024-12-13 08:36:20 +01:00
044c971d00 Implement 2024 day 12 2024-12-12 22:18:32 +01:00
8308580284 Actually run part 2 in Terraform, madness 2024-12-11 23:18:56 +01:00
612c4d19ef Implement efficient algorithm in TF 2024-12-11 22:33:42 +01:00
2beccdd0e6 day 11 part 1 in terraform 2024-12-11 18:53:06 +01:00
023807a701 Implement 2024 day 11 2024-12-11 08:51:32 +01:00
3e709cc863 Implement 2024 day 10 2024-12-10 09:38:31 +01:00
d49fc8e769 Do day 7 RTL
That does indeed cut down branching significantly
2024-12-09 22:30:52 +01:00
cb7aedc4ba Implement 2024 day 9 2024-12-09 18:14:49 +01:00
dcb0f4514d Restructure tests 2024-12-08 10:33:17 +01:00
dfee3d4e11 Implement 2024 day 8 2024-12-08 10:17:29 +01:00
2d49907692 Implement day 5 part 1 in terraform
I'm pretty sure part 2 is impossible
2024-12-07 12:45:41 +01:00
edb0767201 Optimize day 3
Avoid instantiating module and backtracking, instead use regex for
filtering. Much faster. Since we are not matching brackets, but rather
only care about the last instance, the limitations of regex don't apply.
2024-12-07 10:42:38 +01:00
c871a9ea24 Implement 2024 day 7 2024-12-07 10:35:58 +01:00
0c7c54b5c9 Implement 2024 day 6 2024-12-06 09:20:08 +01:00
e9a57701c9 Optimize day 4 enough to run 2024-12-05 22:52:57 +01:00
f7af07a631 Slightly better performance 2024-12-05 22:18:26 +01:00
ecfe5e9f20 Implement day 4 in Terraform 2024-12-05 22:14:35 +01:00
0967a3dfe3 Formatting 2024-12-05 19:36:09 +01:00
bbe3ba8644 Implement 2024 day 5 2024-12-05 08:46:40 +01:00
0ce095e9e5 Implement 2024 day 4 2024-12-04 12:27:06 +01:00
3e07c8563e Liberally use try, document idea 2024-12-03 20:30:19 +01:00
608f3dc621 Implement 2024 day 3 part 2 in Terraform
No one's going to stop me, not even common sense
2024-12-03 19:33:47 +01:00
ff2e421437 formatting 2024-12-03 19:09:04 +01:00
e0d458c972 Forgot to include real input 2024-12-03 08:20:53 +01:00
1c54a79d19 Implement 2024 day 3 part 1 in Terraform 2024-12-03 08:18:15 +01:00
c5cbd6d24f Restructure bonus tests 2024-12-03 08:12:45 +01:00
e36f7dd4f7 Implement 2024 day 3 2024-12-03 08:07:14 +01:00
b2add928ad Implement day 2 in Terraform 2024-12-02 18:25:01 +01:00
5a9667094c Implement 2024 day 2 2024-12-02 08:55:57 +01:00
443ff2cee6 Reduce part 2 to O(n) 2024-12-01 18:04:13 +01:00
f4a5ffe3ce Part 2 and actual testing 2024-12-01 17:05:41 +01:00
73f886359b Implement day 1 part 1 in Terraform 2024-12-01 16:51:45 +01:00
26ee876f7a Minor fixes 2024-12-01 15:21:55 +01:00
16092fe5b2 Implement day 1 2024-12-01 10:48:35 +01:00
6adfbd29f1 Merge pull request #6 from bertptrs/prepare-2024
Prepare for 2024
2024-12-01 10:23:16 +01:00
eef637f006 Remove old unused CI
I'm never going to finish that rework anyway
2024-11-22 10:45:53 +01:00
3d24e50cb3 Ci thing 2024-11-22 10:43:33 +01:00
163 changed files with 6344 additions and 81 deletions

View File

@@ -1,18 +0,0 @@
name: AOC 2019 test suite
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r 2019/requirements.txt
- name: Run Pytest
run: pytest 2019

View File

@@ -1,52 +0,0 @@
on:
- push
name: Advent of Code 2023
jobs:
ci:
strategy:
matrix:
toolchain:
- stable
- beta
experimental: [false]
include:
- toolchain: nightly
experimental: true
name: Continuous Integration
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
steps:
- uses: actions/checkout@v4
- name: Install toolchain
uses: dtolnay/rust-toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.toolchain }}
override: true
components: rustfmt
- name: Set up caching
uses: Swatinem/rust-cache@v2
with:
workspaces: >
2023 -> target
- name: Build binaries
working-directory: 2023
run: >
cargo build --all-targets
- name: Run tests
working-directory: 2023
run: >
cargo test
- name: Check formatting
working-directory: 2023
run: >
cargo fmt --check

3
.gitignore vendored
View File

@@ -67,3 +67,6 @@ flamegraph.svg
# Ignore saved inputs
inputs/
# Terraform dir
.terraform

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("}")

49
2024/bonus/README.md Normal file
View File

@@ -0,0 +1,49 @@
# Bonus Challenge
This part of the repo holds the bonus challenge for 2024: implement as much as possible in pure
Terraform.
## Usage
The infrastructure definitions expect the input files to be located at `../inputs/[\d]{2}.txt`.
After storing the input files there, the code should be runnable as follows:
```console
$ terraform init
< a lot of modules being installed >
$ terraform plan
Changes to Outputs:
+ day01_1 = 42
+ day01_2 = 12
+ day02_1 = …
You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
```
Note that Terraform may freeze for tens of seconds while it's running the computations. This is
normal, and all Terraform code ought to think a little before doing anything. It would save people
some bad rollbacks.
## Development
I use [Terraform tests](https://developer.hashicorp.com/terraform/language/tests) to automatically
run my terraform code on the sample inputs. It's almost a normal development workflow.
The only rule is that the code should be all terraform, no cheating by shelling out to external
programs. Using providers is allowed, as long as the providers don't actually interact with external
systems and are reasonably self-contained. I will try to limit my use of those regardless.
## Why
DevOps will continue until morale improves. But really, a friend remarked that my Python solutions
were strangely normal for me, so I opted to use a language that I do use professionally.
Terraform is a unique beast. It can do a lot, but it is also very limited, and intentionally so.
There's a standard library of functions you might want to use, but all of them work in strange ways
and there isn't that much to begin with. You can never mutate any variables, you can only declare
new ones. You don't have recursion and your only source of loops are list- and map comprehensions,
or multiple instantiations or a module.
These make for a very constrained programming environment, and constrained programming is fun. It
makes you think outside the box.

28
2024/bonus/day01/main.tf Normal file
View File

@@ -0,0 +1,28 @@
variable "input" {
type = string
}
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 : tonumber(line[0])]
right = [for line in local.lines_split : tonumber(line[1])]
left_sorted = sort(local.left)
right_sorted = sort(local.right)
diffs = [for i in range(length(local.left_sorted)) : abs(local.left_sorted[i] - local.right_sorted[i])]
counts = { for num in local.right : num => num... }
matching = [for left in local.left : left * try(length(local.counts[left]), 0)]
}
output "part1" {
value = sum(local.diffs)
}
output "part2" {
value = sum(local.matching)
}

View File

@@ -0,0 +1,17 @@
variable "report" {
type = list(number)
}
module "is_valid" {
source = "../is_valid"
count = length(var.report)
report = concat(
count.index > 0 ? slice(var.report, 0, count.index) : [],
try(slice(var.report, count.index + 1, length(var.report)), [])
)
}
output "valid" {
value = anytrue(module.is_valid[*].valid)
}

View File

@@ -0,0 +1,14 @@
variable "report" {
type = list(number)
}
locals {
delta = [for i in range(1, length(var.report)) : var.report[i] - var.report[i - 1]]
all_negative = alltrue([for d in local.delta : d <= -1 && d >= -3])
all_positive = alltrue([for d in local.delta : d >= 1 && d <= 3])
}
output "valid" {
value = local.all_negative || local.all_positive
}

28
2024/bonus/day02/main.tf Normal file
View File

@@ -0,0 +1,28 @@
variable "input" {
type = string
}
locals {
reports = [for line in split("\n", trim(var.input, "\n")) : split(" ", line)]
}
module "part1_valid" {
source = "./is_valid"
count = length(local.reports)
report = local.reports[count.index]
}
module "part2_valid" {
source = "./is_savable"
count = length(local.reports)
report = local.reports[count.index]
}
output "part1" {
value = length([for i in range(length(local.reports)) : true if module.part1_valid[i].valid])
}
output "part2" {
value = length([for i in range(length(local.reports)) : true if module.part2_valid[i].valid])
}

19
2024/bonus/day03/main.tf Normal file
View File

@@ -0,0 +1,19 @@
variable "input" {
type = string
}
locals {
filtered = replace(var.input, "/(?s)don't\\(\\).*?do\\(\\)/", "")
filtered2 = replace(local.filtered, "/(?s)don't\\(\\).*/", "")
muls = regexall("mul\\((\\d+),(\\d+)\\)", var.input)
filtered_muls = regexall("mul\\((\\d+),(\\d+)\\)", local.filtered2)
}
output "part1" {
value = sum([for mul in local.muls : tonumber(mul[1]) * tonumber(mul[0])])
}
output "part2" {
value = sum([for mul in local.filtered_muls : tonumber(mul[1]) * tonumber(mul[0])])
}

View File

@@ -0,0 +1,30 @@
variable "grid" {
type = list(string)
}
variable "x" {
type = number
}
variable "y" {
type = number
}
locals {
found_a = substr(var.grid[var.y], var.x, 1) == "A"
c1 = substr(var.grid[var.y - 1], var.x - 1, 1)
c2 = substr(var.grid[var.y - 1], var.x + 1, 1)
c3 = substr(var.grid[var.y + 1], var.x + 1, 1)
c4 = substr(var.grid[var.y + 1], var.x - 1, 1)
d1 = "${local.c1}${local.c3}"
d2 = "${local.c2}${local.c4}"
found_d1 = contains(["SM", "MS"], local.d1)
found_d2 = contains(["SM", "MS"], local.d2)
}
output "found" {
value = local.found_a && local.found_d1 && local.found_d2
}

View File

@@ -0,0 +1,27 @@
variable "grid" {
type = list(string)
}
variable "x" {
type = number
}
variable "y" {
type = number
}
variable "dx" {
type = number
}
variable "dy" {
type = number
}
locals {
word = join("", [for i in range(4) : var.x + i * var.dx >= 0 ? try(substr(var.grid[var.y + i * var.dy], var.x + i * var.dx, 1), "F") : "F"])
}
output "found" {
value = contains(["SAMX", "XMAS"], local.word) ? 1 : 0
}

View File

@@ -0,0 +1,59 @@
variable "grid" {
type = list(string)
}
variable "index" {
type = number
}
variable "width" {
type = number
}
variable "height" {
type = number
}
locals {
x = var.index % var.width
y = floor(var.index / var.width)
directions = {
"DL" = [-1, 1]
"D" = [0, 1]
"DR" = [1, 1]
}
should_check_x_mas = local.x >= 1 && local.y >= 1 && local.x < var.width - 1 && local.y < var.height - 1
}
module "check_xmas" {
source = "./check_xmas"
for_each = local.directions
grid = var.grid
x = local.x
y = local.y
dx = each.value[0]
dy = each.value[1]
}
module "check_x_mas" {
source = "./check_x_mas"
count = local.should_check_x_mas ? 1 : 0
grid = var.grid
y = local.y
x = local.x
}
output "xmas" {
value = sum([for _, v in module.check_xmas : v.found])
}
output "x_mas" {
value = try(module.check_x_mas[0].found, false) ? 1 : 0
}

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

@@ -0,0 +1,31 @@
variable "input" {
type = string
}
locals {
grid = split("\n", chomp(var.input))
height = length(local.grid)
width = length(local.grid[0])
lr = length(regexall("XMAS", var.input))
rl = length(regexall("SAMX", var.input))
}
module "check_point" {
source = "./check_point"
count = local.width * local.height
width = local.width
height = local.height
grid = local.grid
index = count.index
}
output "part1" {
value = sum(module.check_point[*].xmas) + local.lr + local.rl
}
output "part2" {
value = sum(module.check_point[*].x_mas)
}

25
2024/bonus/day05/main.tf Normal file
View File

@@ -0,0 +1,25 @@
variable "input" {
type = string
}
locals {
parts = split("\n\n", chomp(var.input))
rules = [for rule_line in split("\n", local.parts[0]) : [for v in split("|", rule_line) : tonumber(v)]]
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)]]
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(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]...))
}

24
2024/bonus/day11/main.tf Normal file
View File

@@ -0,0 +1,24 @@
variable "input" {
type = string
}
locals {
nums = [for s in split(" ", chomp(var.input)) : tonumber(s)]
grouped = { for num in local.nums : num => 1... }
total = { for k, v in local.grouped : k => sum(v) }
}
module "step1" {
source = "./step"
prev = local.total
}
output "part1" {
value = sum(values(module.step25.next))
}
output "part2" {
value = sum(values(module.step75.next))
}

View File

@@ -0,0 +1,32 @@
variable "prev" {
type = map(number)
}
locals {
by_value = flatten([
for num, count in var.prev : (
tonumber(num) == 0
? [{ number = 1, amount = count }]
: (
length(tostring(num)) % 2 == 0
? [
{
number = tonumber(substr(tostring(num), 0, length(tostring(num)) / 2)),
amount = count
},
{
number = tonumber(substr(tostring(num), length(tostring(num)) / 2, length(tostring(num)) / 2)),
amount = count,
},
]
: [{ number = 2024 * num, amount = count }]
)
)
])
grouped = { for kv in local.by_value : kv.number => kv.amount... }
}
output "next" {
value = { for num, groups in local.grouped : num => sum(groups) }
}

446
2024/bonus/day11/steps.tf Normal file
View File

@@ -0,0 +1,446 @@
# This file was generated using a nasty bash for loop. Edit at your own peril.
module "step2" {
source = "./step"
prev = module.step1.next
}
module "step3" {
source = "./step"
prev = module.step2.next
}
module "step4" {
source = "./step"
prev = module.step3.next
}
module "step5" {
source = "./step"
prev = module.step4.next
}
module "step6" {
source = "./step"
prev = module.step5.next
}
module "step7" {
source = "./step"
prev = module.step6.next
}
module "step8" {
source = "./step"
prev = module.step7.next
}
module "step9" {
source = "./step"
prev = module.step8.next
}
module "step10" {
source = "./step"
prev = module.step9.next
}
module "step11" {
source = "./step"
prev = module.step10.next
}
module "step12" {
source = "./step"
prev = module.step11.next
}
module "step13" {
source = "./step"
prev = module.step12.next
}
module "step14" {
source = "./step"
prev = module.step13.next
}
module "step15" {
source = "./step"
prev = module.step14.next
}
module "step16" {
source = "./step"
prev = module.step15.next
}
module "step17" {
source = "./step"
prev = module.step16.next
}
module "step18" {
source = "./step"
prev = module.step17.next
}
module "step19" {
source = "./step"
prev = module.step18.next
}
module "step20" {
source = "./step"
prev = module.step19.next
}
module "step21" {
source = "./step"
prev = module.step20.next
}
module "step22" {
source = "./step"
prev = module.step21.next
}
module "step23" {
source = "./step"
prev = module.step22.next
}
module "step24" {
source = "./step"
prev = module.step23.next
}
module "step25" {
source = "./step"
prev = module.step24.next
}
module "step26" {
source = "./step"
prev = module.step25.next
}
module "step27" {
source = "./step"
prev = module.step26.next
}
module "step28" {
source = "./step"
prev = module.step27.next
}
module "step29" {
source = "./step"
prev = module.step28.next
}
module "step30" {
source = "./step"
prev = module.step29.next
}
module "step31" {
source = "./step"
prev = module.step30.next
}
module "step32" {
source = "./step"
prev = module.step31.next
}
module "step33" {
source = "./step"
prev = module.step32.next
}
module "step34" {
source = "./step"
prev = module.step33.next
}
module "step35" {
source = "./step"
prev = module.step34.next
}
module "step36" {
source = "./step"
prev = module.step35.next
}
module "step37" {
source = "./step"
prev = module.step36.next
}
module "step38" {
source = "./step"
prev = module.step37.next
}
module "step39" {
source = "./step"
prev = module.step38.next
}
module "step40" {
source = "./step"
prev = module.step39.next
}
module "step41" {
source = "./step"
prev = module.step40.next
}
module "step42" {
source = "./step"
prev = module.step41.next
}
module "step43" {
source = "./step"
prev = module.step42.next
}
module "step44" {
source = "./step"
prev = module.step43.next
}
module "step45" {
source = "./step"
prev = module.step44.next
}
module "step46" {
source = "./step"
prev = module.step45.next
}
module "step47" {
source = "./step"
prev = module.step46.next
}
module "step48" {
source = "./step"
prev = module.step47.next
}
module "step49" {
source = "./step"
prev = module.step48.next
}
module "step50" {
source = "./step"
prev = module.step49.next
}
module "step51" {
source = "./step"
prev = module.step50.next
}
module "step52" {
source = "./step"
prev = module.step51.next
}
module "step53" {
source = "./step"
prev = module.step52.next
}
module "step54" {
source = "./step"
prev = module.step53.next
}
module "step55" {
source = "./step"
prev = module.step54.next
}
module "step56" {
source = "./step"
prev = module.step55.next
}
module "step57" {
source = "./step"
prev = module.step56.next
}
module "step58" {
source = "./step"
prev = module.step57.next
}
module "step59" {
source = "./step"
prev = module.step58.next
}
module "step60" {
source = "./step"
prev = module.step59.next
}
module "step61" {
source = "./step"
prev = module.step60.next
}
module "step62" {
source = "./step"
prev = module.step61.next
}
module "step63" {
source = "./step"
prev = module.step62.next
}
module "step64" {
source = "./step"
prev = module.step63.next
}
module "step65" {
source = "./step"
prev = module.step64.next
}
module "step66" {
source = "./step"
prev = module.step65.next
}
module "step67" {
source = "./step"
prev = module.step66.next
}
module "step68" {
source = "./step"
prev = module.step67.next
}
module "step69" {
source = "./step"
prev = module.step68.next
}
module "step70" {
source = "./step"
prev = module.step69.next
}
module "step71" {
source = "./step"
prev = module.step70.next
}
module "step72" {
source = "./step"
prev = module.step71.next
}
module "step73" {
source = "./step"
prev = module.step72.next
}
module "step74" {
source = "./step"
prev = module.step73.next
}
module "step75" {
source = "./step"
prev = module.step74.next
}

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
}

13
2024/bonus/day19/main.tf Normal file
View File

@@ -0,0 +1,13 @@
variable "input" {
type = string
}
locals {
parts = split("\n\n", chomp((var.input)))
patterns = replace(local.parts[0], ", ", "|")
valid = [for line in split("\n", local.parts[1]) : line if length(regexall("^(${local.patterns})+$", line)) > 0]
}
output "part1" {
value = length(local.valid)
}

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)])
}

133
2024/bonus/main.tf Normal file
View File

@@ -0,0 +1,133 @@
terraform {
}
module "day01" {
source = "./day01"
input = file("../inputs/01.txt")
}
output "day01_1" {
value = module.day01.part1
}
output "day01_2" {
value = module.day01.part2
}
module "day02" {
source = "./day02"
input = file("../inputs/02.txt")
}
output "day02_1" {
value = module.day02.part1
}
output "day02_2" {
value = module.day02.part2
}
module "day03" {
source = "./day03"
input = file("../inputs/03.txt")
}
output "day03_1" {
value = module.day03.part1
}
output "day03_2" {
value = module.day03.part2
}
# Don't run this, it runs forever (6 minutes) and requires a lot of memory (~5.5GB) to execute to
# boot. Trust me, it works.
# module "day04" {
# source = "./day04"
# input = file("../inputs/04.txt")
# }
# output "day04_1" {
# value = module.day04.part1
# }
# output "day04_2" {
# value = module.day04.part2
# }
module "day05" {
source = "./day05"
input = file("../inputs/05.txt")
}
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")
}
output "day11_1" {
value = module.day11.part1
}
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")
}
output "day19_1" {
value = module.day19.part1
}
module "day25" {
source = "./day25"
input = file("../inputs/25.txt")
}
output "day25_1" {
value = module.day25.part1
}

242
2024/bonus/tests.tftest.hcl Normal file
View File

@@ -0,0 +1,242 @@
run "day1" {
command = plan
module {
source = "./day01"
}
variables {
input = file("../tests/samples/01.txt")
}
assert {
condition = output.part1 == 11
error_message = "Part1 output is wrong"
}
assert {
condition = output.part2 == 31
error_message = "Part2 output is wrong"
}
}
run "day2" {
command = plan
module {
source = "./day02"
}
variables {
input = file("../tests/samples/02.txt")
}
assert {
condition = output.part1 == 2
error_message = "Part1 output is wrong"
}
assert {
condition = output.part2 == 4
error_message = "Part2 output is wrong"
}
}
run "day3_1" {
command = plan
module {
source = "./day03"
}
variables {
input = "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))"
}
assert {
condition = output.part1 == 161
error_message = "Part1 output is wrong"
}
}
run "day3_2" {
command = plan
module {
source = "./day03"
}
variables {
input = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"
}
assert {
condition = output.part2 == 48
error_message = "Part2 output is wrong"
}
}
run "day4_small" {
command = plan
module {
source = "./day04"
}
variables {
input = file("../tests/samples/04.1.txt")
}
assert {
condition = output.part1 == 4
error_message = "Part1 output is wrong"
}
}
run "day4" {
command = plan
module {
source = "./day04"
}
variables {
input = file("../tests/samples/04.2.txt")
}
assert {
condition = output.part1 == 18
error_message = "Part1 output is wrong"
}
assert {
condition = output.part2 == 9
error_message = "Part2 output is wrong"
}
}
run "day5_1" {
command = plan
module {
source = "./day05"
}
variables {
input = file("../tests/samples/05.txt")
}
assert {
condition = output.part1 == 143
error_message = "Part1 output is wrong"
}
}
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
module {
source = "./day11"
}
variables {
input = "125 17"
}
assert {
condition = output.part1 == 55312
error_message = "Part1 output is wrong"
}
}
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
module {
source = "./day19"
}
variables {
input = file("../tests/samples/19.txt")
}
assert {
condition = output.part1 == 6
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"
}
}

0
2024/inputs/.gitkeep Normal file
View File

View File

@@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"click>=8.1.7",
"networkx>=3.4.2",
"numpy>=2.1.2",
]

View File

@@ -1,5 +1,6 @@
import datetime
import time
from typing import IO
import click
@@ -15,14 +16,17 @@ from aoc import days
default="-",
help="Problem input file",
)
@click.option("-t", "--time", "timing", is_flag=True, help="Print elapsed time afterwards")
@click.option(
"-t", "--time", "timing", is_flag=True, help="Print elapsed time afterwards"
)
@click.argument("day", required=True)
def main(day: int, timing: bool, data: str) -> None:
def main(day: int, timing: bool, data: IO[str]) -> None:
runner_class = days.get_runner(day)
contents = data.read()
start = time.perf_counter_ns()
part1, part2 = runner_class.run_both(data)
part1, part2 = runner_class.run_both(contents)
if timing:
elapsed = time.perf_counter_ns() - start

View File

@@ -1,13 +1,27 @@
from collections import defaultdict
from io import StringIO
from typing import Any
from . import SeparateRunner
import numpy
from . import CombinedRunner
class DayRunner(SeparateRunner):
class DayRunner(CombinedRunner):
@classmethod
def part1(cls, _data: str) -> Any:
return "Hello"
def run_both(cls, data: str) -> tuple[Any, Any]:
nums = numpy.loadtxt(StringIO(data), dtype=numpy.int32)
@classmethod
def part2(cls, _data: str) -> Any:
return "world!"
left = nums[..., 0]
right = nums[..., 1]
left.sort()
right.sort()
diff = numpy.abs(left - right).sum()
counts: defaultdict[int, int] = defaultdict(int)
for val in right:
counts[val] += 1
return diff, sum(counts[v] * v for v in left)

View File

@@ -0,0 +1,85 @@
from collections import deque
import numpy
from . import SeparateRunner
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
grid = numpy.array(
[[int(v) for v in line] for line in input.strip().split("\n")]
)
width, height = grid.shape
start_x, start_y = numpy.nonzero(grid == 0)
todo = []
reachable = 0
for sx, sy in zip(start_x, start_y):
todo.append((sx, sy))
ways = numpy.zeros_like(grid, dtype=bool)
def enqueue(x: int, y: int, val: int) -> None:
if grid[x, y] == val + 1:
if not ways[x, y]:
todo.append((x, y))
ways[x, y] += True
while todo:
x, y = todo.pop()
val = grid[x, y]
if val == 9:
reachable += 1
continue
if x > 0:
enqueue(x - 1, y, val)
if y > 0:
enqueue(x, y - 1, val)
if x < width - 1:
enqueue(x + 1, y, val)
if y < height - 1:
enqueue(x, y + 1, val)
return reachable
@classmethod
def part2(cls, input: str) -> int:
grid = numpy.array(
[[int(v) for v in line] for line in input.strip().split("\n")]
)
ways = numpy.zeros_like(grid)
width, height = grid.shape
start_x, start_y = numpy.nonzero(grid == 9)
ways[grid == 9] = 1
todo = deque((x, y) for x, y in zip(start_x, start_y))
def enqueue(x: int, y: int, val: int, cur: int) -> None:
if grid[x, y] == val - 1:
if ways[x, y] == 0:
todo.append((x, y))
ways[x, y] += cur
while todo:
x, y = todo.popleft()
val = grid[x, y]
cur = ways[x, y]
if x > 0:
enqueue(x - 1, y, val, cur)
if y > 0:
enqueue(x, y - 1, val, cur)
if x < width - 1:
enqueue(x + 1, y, val, cur)
if y < height - 1:
enqueue(x, y + 1, val, cur)
return ways[grid == 0].sum()

View File

@@ -0,0 +1,50 @@
import functools
from collections import defaultdict
from . import CombinedRunner
@functools.cache
def blink_num(num: int) -> tuple[int, ...]:
if num == 0:
return (1,)
num_str = str(num)
num_len = len(num_str)
if num_len % 2 == 0:
half = num_len // 2
return (int(num_str[:half]), int(num_str[half:]))
return (num * 2024,)
def step(nums: dict[int, int]) -> dict[int, int]:
result = defaultdict(int)
for num, count in nums.items():
for transformed in blink_num(num):
result[transformed] += count
return result
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, int]:
nums = [int(val) for val in input.strip().split(" ")]
counts = defaultdict(int)
for num in nums:
counts[num] += 1
for _ in range(25):
counts = step(counts)
part1 = sum(counts.values())
for _ in range(50):
counts = step(counts)
return part1, sum(counts.values())

101
2024/src/aoc/days/day12.py Normal file
View File

@@ -0,0 +1,101 @@
import numpy
from . import CombinedRunner
DIRECTIONS = [
(-1, 0),
(1, 0),
(0, -1),
(0, 1),
]
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, int]:
grid = numpy.array(list(map(list, input.strip().split("\n"))))
score = 0
score2 = 0
for y in range(grid.shape[0]):
for x in range(grid.shape[1]):
if grid[y, x] == ".":
continue
search = grid[y, x]
grid[y, x] = "."
todo = [(y, x)]
cluster = {(y, x)}
def enqueue(y, x):
if grid[y, x] == search:
grid[y, x] = "."
todo.append((y, x))
cluster.add((y, x))
while todo:
cy, cx = todo.pop()
if cx > 0:
enqueue(cy, cx - 1)
if cy > 0:
enqueue(cy - 1, cx)
if cx < grid.shape[1] - 1:
enqueue(cy, cx + 1)
if cy < grid.shape[0] - 1:
enqueue(cy + 1, cx)
side_length = sum(
sum((cy + dy, cx + dx) not in cluster for dy, dx in DIRECTIONS)
for cy, cx in cluster
)
corners = 0
for cy, cx in cluster:
# Outer corners
corners += (cy, cx - 1) not in cluster and (
cy - 1,
cx,
) not in cluster
corners += (cy, cx + 1) not in cluster and (
cy - 1,
cx,
) not in cluster
corners += (cy, cx - 1) not in cluster and (
cy + 1,
cx,
) not in cluster
corners += (cy, cx + 1) not in cluster and (
cy + 1,
cx,
) not in cluster
# Inner corners
corners += (
(cy, cx - 1) in cluster
and (cy - 1, cx) in cluster
and (cy - 1, cx - 1) not in cluster
)
corners += (
(cy, cx + 1) in cluster
and (cy - 1, cx) in cluster
and (cy - 1, cx + 1) not in cluster
)
corners += (
(cy, cx - 1) in cluster
and (cy + 1, cx) in cluster
and (cy + 1, cx - 1) not in cluster
)
corners += (
(cy, cx + 1) in cluster
and (cy + 1, cx) in cluster
and (cy + 1, cx + 1) not in cluster
)
score += side_length * len(cluster)
score2 += corners * len(cluster)
return (score, score2)

View File

@@ -0,0 +1,43 @@
import re
import numpy
from . import CombinedRunner
NASTY_REGEX = r"""Button A: X\+(\d+), Y\+(\d+)
Button B: X\+(\d+), Y\+(\d+)
Prize: X=(\d+), Y=(\d+)"""
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, int]:
machines = re.findall(NASTY_REGEX, input)
cost_to_win = 0
cost_to_win2 = 0
scale = 10000000000000
for machine in machines:
ax, ay, bx, by, px, py = map(int, machine)
X = numpy.array([[ax, bx], [ay, by]])
B = numpy.array([px, py])
B2 = numpy.array([px + scale, py + scale])
A = numpy.linalg.solve(X, B)
A2 = numpy.linalg.solve(X, B2)
a_press, b_press = map(round, A)
a_press2, b_press2 = map(round, A2)
if a_press * ax + b_press * bx == px and a_press * ay + b_press * by == py:
cost_to_win += 3 * a_press + b_press
if (
a_press2 * ax + b_press2 * bx == px + scale
and a_press2 * ay + b_press2 * by == py + scale
):
cost_to_win2 += 3 * a_press2 + b_press2
return cost_to_win, cost_to_win2

View File

@@ -0,0 +1,80 @@
import itertools
import math
import re
import sys
import numpy
from . import SeparateRunner
NASTY_REGEX = re.compile(r"p=(-?\d+),(-?\d+) v=(-?\d+),(-?\d+)")
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str, width: int = 101, height: int = 103) -> int:
points = NASTY_REGEX.findall(input)
quadrants = [0] * 4
x_middle = width // 2
y_middle = height // 2
for point in points:
x, y, dx, dy = map(int, point)
rx = ((x + dx * 100) % width + width) % width
ry = ((y + dy * 100) % height + height) % height
match rx:
case _ if rx < x_middle:
xq = 0
case _ if rx > x_middle:
xq = 1
case _:
continue
match ry:
case _ if ry < y_middle:
yq = 0
case _ if ry > y_middle:
yq = 1
case _:
continue
quadrants[2 * yq + xq] += 1
return math.prod(quadrants)
@classmethod
def part2(cls, input: str) -> int:
width = 101
height = 103
points = NASTY_REGEX.findall(input)
points_fast = numpy.array([list(map(int, point)) for point in points])
positions = points_fast[:, 0:2]
velocities = points_fast[:, 2:]
target = len(velocities)
# Assumption: when the easter egg happens, no robots overlap, and this is the
# only time this happens. There is no reason this should work but it does.
mod_base = numpy.array([width, height])
for i in itertools.count(1):
positions += velocities
positions %= mod_base
positions += mod_base
positions %= mod_base
if len(numpy.unique(positions, axis=0)) == target:
grid = [[" "] * width for _ in range(height)]
for x, y in positions:
grid[y][x] = "#"
tree = "\n".join(map((lambda x: "".join(x)), grid))
print(tree, file=sys.stderr)
return i

161
2024/src/aoc/days/day15.py Normal file
View File

@@ -0,0 +1,161 @@
import itertools
import numpy
from . import SeparateRunner
def parse_input(data: str) -> tuple[numpy.array, str]:
grid, steps = data.split("\n\n")
grid_split = numpy.array([list(line) for line in grid.split("\n")])
steps = "".join(steps.split("\n"))
return grid_split, steps
def convert_dir(c: str) -> tuple[int, int]:
match c:
case "^":
return 0, -1
case ">":
return 1, 0
case "<":
return -1, 0
case "v":
return 0, 1
case other:
raise ValueError(f"Invalid movement: {other}")
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> None:
grid, steps = parse_input(input)
y, x = numpy.where(grid == "@")
x, y = x[0], y[0]
for c in steps:
dx, dy = convert_dir(c)
match grid[y + dy, x + dx]:
case "#":
continue
case "O":
crashed = False
for dist in itertools.count(2):
match grid[y + dist * dy, x + dist * dx]:
case "O":
continue
case "#":
crashed = True
break
case _:
crashed = False
break
if crashed:
continue
grid[y + dist * dy, x + dist * dx] = "O"
case _:
pass
grid[y, x] = "."
x += dx
y += dy
grid[y, x] = "@"
stones = numpy.where(grid == "O")
return sum(100 * y + x for y, x in zip(*stones))
@classmethod
def part2(cls, input: str) -> None:
input = input.replace(".", "..")
input = input.replace("#", "##")
input = input.replace("O", "[]")
input = input.replace("@", "@.")
grid, steps = parse_input(input)
y, x = numpy.where(grid == "@")
x, y = x[0], y[0]
for c in steps:
dx, dy = convert_dir(c)
match grid[y + dy, x + dx]:
case "#":
continue
case "]" | "[":
crashed = False
if dy == 0:
# easy case: just move linearly
for dist in itertools.count(2):
match grid[y, x + dist * dx]:
case "[" | "]":
continue
case "#":
crashed = True
break
case _:
break
if crashed:
continue
# shuffle all grid points one over
for steps in range(dist, 1, -1):
grid[y, x + dx * steps] = grid[y, x + dx * (steps - 1)]
else:
if grid[y + dy, x] == "[":
to_check = {x, x + 1}
else:
to_check = {x, x - 1}
moving_stones = [to_check]
for dist in itertools.count(2):
to_check_next = set()
for cx in to_check:
match grid[y + dist * dy, cx]:
case "#":
crashed = True
break
case "[":
to_check_next.add(cx)
to_check_next.add(cx + 1)
case "]":
to_check_next.add(cx)
to_check_next.add(cx - 1)
case _:
continue
if crashed or not to_check_next:
break
moving_stones.append(to_check_next)
to_check = to_check_next
if crashed:
continue
for steps in range(len(moving_stones), 0, -1):
dist = steps + 1
for cx in moving_stones[steps - 1]:
grid[y + dy * dist, cx] = grid[y + dy * (dist - 1), cx]
grid[y + dy * (dist - 1), cx] = "."
case _:
pass
grid[y, x] = "."
x += dx
y += dy
grid[y, x] = "@"
stones = numpy.where(grid == "[")
return sum(100 * y + x for y, x in zip(*stones))

View File

@@ -0,0 +1,76 @@
import heapq
import numpy
from . import CombinedRunner
TURNS = (
(-1, 1),
(1, -1),
)
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, int]:
grid = numpy.array([list(line) for line in input.strip().split("\n")])
y, x = numpy.where(grid == "S")
x, y = x[0], y[0]
todo = [(0, x, y, 1, 0)]
best = {
(x, y, 1, 0): (0, []),
}
def enqueue(dist, x, y, dx, dy, cx, cy, cdx, cdy):
if grid[y, x] == "#":
return
if (x, y, dx, dy) not in best or best[x, y, dx, dy][0] > dist:
best[x, y, dx, dy] = (dist, [(cx, cy, cdx, cdy)])
heapq.heappush(todo, (dist, x, y, dx, dy))
elif best[x, y, dx, dy][0] == dist:
best[x, y, dx, dy][1].append((cx, cy, cdx, cdy))
shortest_dist = None
finishes = set()
while todo:
dist, x, y, dx, dy = heapq.heappop(todo)
if best[x, y, dx, dy][0] < dist:
continue
if shortest_dist is not None and shortest_dist < dist:
break
if grid[y, x] == "E":
shortest_dist = dist
finishes.add((x, y, dx, dy))
enqueue(dist + 1, x + dx, y + dy, dx, dy, x, y, dx, dy)
enqueue(dist + 2001, x - dx, y - dy, dx, dy, x, y, dx, dy)
for tx, ty in TURNS:
ndx = dy * ty
ndy = dx * ty
enqueue(dist + 1001, x + ndx, y + ndy, ndx, ndy, x, y, dx, dy)
assert shortest_dist is not None, "Should find a path to the exit"
visited_tiles = {(x, y) for x, y, _, _ in finishes}
todo2 = [f for f in finishes]
visited_states = set(todo2)
while todo2:
state = todo2.pop()
for prev in best[state][1]:
if prev not in visited_states:
visited_states.add(prev)
visited_tiles.add((prev[0], prev[1]))
todo2.append(prev)
return shortest_dist, len(visited_tiles)

View File

@@ -0,0 +1,89 @@
import re
from . import SeparateRunner
def run_program(
register_a: int, register_b: int, register_c: int, program: list[int]
) -> list[int]:
ip = 0
out = []
def combo(index: int) -> int:
match program[index]:
case 0:
return 0
case 1:
return 1
case 2:
return 2
case 3:
return 3
case 4:
return register_a
case 5:
return register_b
case 6:
return register_c
while ip < len(program):
match program[ip]:
case 0: # adv
register_a = register_a // 2 ** combo(ip + 1)
case 1: # bxl
register_b ^= program[ip + 1]
case 2: # bst
register_b = combo(ip + 1) & 0x7
case 3: # jnz
if register_a != 0:
ip = program[ip + 1]
continue
case 4: # bxc
register_b ^= register_c
case 5: # out
out.append(combo(ip + 1) & 7)
case 6: # bdv
register_b = register_a // 2 ** combo(ip + 1)
case 7: # cdv
register_c = register_a // 2 ** combo(ip + 1)
ip += 2
return out
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> str:
numbers = re.findall(r"\d+", input)
register_a, register_b, register_c = map(int, numbers[:3])
program = list(map(int, numbers[3:]))
out = run_program(register_a, register_b, register_c, program)
return ",".join(map(str, out))
@classmethod
def part2(cls, input: str) -> int:
numbers = re.findall(r"\d+", input)
_, register_b, register_c = map(int, numbers[:3])
program = list(map(int, numbers[3:]))
cur = [0]
# It came to me in a dream
for entry in reversed(program):
next_gen = []
for num in cur:
num *= 8
for n in range(8):
output = run_program(num + n, register_b, register_c, program)
result = output[0]
if result == entry:
next_gen.append(num + n)
cur = next_gen
return cur[0]

View File

@@ -0,0 +1,70 @@
from collections import deque
from . import SeparateRunner
def parse_input(data: str) -> list[tuple[int, int]]:
return [tuple(map(int, line.split(","))) for line in data.strip().split("\n")]
def find_exit(fallen: set[tuple[int, int]], width: int, height: int) -> int | None:
todo = deque([(0, 0, 0)])
best = {(0, 0): 0}
def enqueue(dist: int, x: int, y: int):
# print(f"trying {x},{y}")
if (x, y) in fallen:
return
if (x, y) not in best or best[x, y] > dist:
best[x, y] = dist
todo.append((dist, x, y))
while todo:
dist, x, y = todo.popleft()
# print(x, y)
if x == width - 1 and y == height - 1:
return dist
if x > 0:
enqueue(dist + 1, x - 1, y)
if x + 1 < width:
enqueue(dist + 1, x + 1, y)
if y > 0:
enqueue(dist + 1, x, y - 1)
if y + 1 < height:
enqueue(dist + 1, x, y + 1)
class DayRunner(SeparateRunner):
@classmethod
def part1(
cls, input: str, width: int = 71, height: int = 71, limit: int = 1024
) -> int:
falling = parse_input(input)
return find_exit(set(falling[:limit]), width, height)
@classmethod
def part2(cls, input: str, width: int = 71, height: int = 71) -> str:
falling = parse_input(input)
lower = 0
upper = len(falling)
while lower < upper:
mid = lower + (upper - lower) // 2
if find_exit(set(falling[:mid]), width, height) is not None:
lower = mid + 1
else:
upper = mid
first_blocker = falling[lower - 1]
return f"{first_blocker[0]},{first_blocker[1]}"

View File

@@ -0,0 +1,41 @@
import collections
import functools
from . import CombinedRunner
def parse_input(data: str) -> tuple[list[str], list[str]]:
patterns, designs = data.strip().split("\n\n")
return patterns.split(", "), designs.split("\n")
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> int:
patterns, designs = parse_input(input)
by_prefix = collections.defaultdict(list)
for prefix in patterns:
by_prefix[prefix[0]].append(prefix)
possible = 0
ways = 0
@functools.cache
def is_possible(design: str) -> bool:
if not design:
return 1
else:
return sum(
is_possible(design[len(prefix) :])
for prefix in by_prefix[design[0]]
if design.startswith(prefix)
)
for design in designs:
if (solve := is_possible(design)) > 0:
possible += 1
ways += solve
return possible, ways

47
2024/src/aoc/days/day2.py Normal file
View File

@@ -0,0 +1,47 @@
import numpy
from . import SeparateRunner
def _safe(nums: numpy.ndarray) -> bool:
steps = nums[1:] - nums[:-1]
if numpy.all(steps > 0):
return numpy.all((steps >= 1) & (steps <= 3))
elif numpy.all(steps < 0):
return numpy.all((steps <= -1) & (steps >= -3))
else:
return False
def is_safe(line: str) -> bool:
nums = numpy.fromstring(line, dtype=numpy.int32, sep=" ")
return _safe(nums)
def is_savable(line: str) -> bool:
nums = numpy.fromstring(line, dtype=numpy.int32, sep=" ")
return any(
_safe(numpy.concatenate((nums[:i], nums[i + 1 :]), axis=None))
for i in range(len(nums))
)
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, data: str) -> int:
lines = data.strip().split("\n")
safe = sum(1 for line in lines if is_safe(line))
return safe
@classmethod
def part2(cls, data: str) -> int:
lines = data.strip().split("\n")
safe = sum(1 for line in lines if is_savable(line))
return safe

View File

@@ -0,0 +1,82 @@
import itertools
import numpy
from . import SeparateRunner
DIRECTIONS = [
(-1, 0),
(1, 0),
(0, -1),
(0, 1),
]
CHEATS = [
(-2, 0),
(2, 0),
(0, -2),
(0, 2),
]
def parse_path(input: str) -> dict[tuple[int, int], int]:
grid = numpy.array(list(map(list, input.strip().split("\n"))))
ys, xs = numpy.nonzero(grid == "S")
sx, sy = int(xs[0]), int(ys[0])
nx, ny = sx, sy
path = {
(sx, sy): 0,
}
while grid[ny, nx] != "E":
x, y = nx, ny
for dx, dy in DIRECTIONS:
if grid[y + dy, x + dx] == "#" or (x + dx, y + dy) in path:
continue
nx = x + dx
ny = y + dy
break
path[nx, ny] = len(path)
return path
def get_savings(a: tuple[tuple[int, int], int], b: tuple[tuple[int, int], int]) -> int:
(ax, ay), ad = a
(bx, by), bd = b
dist = abs(bx - ax) + abs(by - ay)
if dist <= 20:
return bd - ad - dist
else:
return 0
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str, limit: int = 100) -> int:
path = parse_path(input)
total = 0
for (px, py), dist in path.items():
for dx, dy in CHEATS:
if (other := path.get((px + dx, py + dy))) is not None:
savings = dist - other - 2
if savings >= limit:
total += 1
return total
@classmethod
def part2(cls, input: str, limit: int = 100) -> int:
path = parse_path(input)
return sum(
get_savings(a, b) >= limit
for a, b in itertools.combinations(path.items(), 2)
)

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

View File

@@ -0,0 +1,41 @@
from collections import defaultdict
import numpy
from numpy.lib.stride_tricks import sliding_window_view
from . import CombinedRunner
def advance(secrets: numpy.array) -> numpy.array:
new_secrets = (secrets ^ (secrets << 6)) & 0xFFFFFF
new_secrets ^= new_secrets >> 5
new_secrets ^= new_secrets << 11
new_secrets &= 0xFFFFFF
return new_secrets
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, int]:
secrets = numpy.fromstring(input, dtype=int, sep="\n")
progression = [secrets]
for _ in range(2000):
secrets = advance(secrets)
progression.append(secrets)
field = numpy.stack(progression, axis=-1) % 10
delta = field[:, 1:] - field[:, :-1]
windows = sliding_window_view(delta, 4, axis=1)
per_signal = defaultdict(int)
for row_scores, row_deltas in zip(field, windows):
unique, positions = numpy.unique(row_deltas, return_index=True, axis=0)
for key, bananas in zip(unique, row_scores[positions + 4]):
per_signal[tuple(key)] += bananas
return secrets.sum(), max(per_signal.values())

View File

@@ -0,0 +1,42 @@
from collections import defaultdict
import networkx
from . import SeparateRunner
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
edges = defaultdict(set)
for line in input.strip().split("\n"):
a, b = line.split("-")
edges[a].add(b)
edges[b].add(a)
found = set()
for a, out in edges.items():
if a[0] != "t":
continue
for b in out:
for c in edges[b]:
if c in out:
found.add(tuple(sorted([a, b, c])))
return len(found)
@classmethod
def part2(cls, input: str) -> str:
graph = networkx.Graph()
for line in input.strip().split("\n"):
a, b = line.split("-")
graph.add_edge(a, b)
cliques = networkx.find_cliques(graph)
max_clique = max(cliques, key=len)
return ",".join(sorted(max_clique))

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

25
2024/src/aoc/days/day3.py Normal file
View File

@@ -0,0 +1,25 @@
import re
from . import SeparateRunner
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, data: str) -> int:
return sum(int(a) * int(b) for a, b in re.findall(r"mul\((\d+),(\d+)\)", data))
@classmethod
def part2(cls, data: str) -> int:
do = True
total = 0
for op, a, b in re.findall(r"(don't\(\)|do\(\)|mul\((\d+),(\d+)\))", data):
match op:
case "do()":
do = True
case "don't()":
do = False
case _:
if do:
total += int(a) * int(b)
return total

69
2024/src/aoc/days/day4.py Normal file
View File

@@ -0,0 +1,69 @@
import numpy
from . import SeparateRunner
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
grid = numpy.array(list(map(list, input.strip().split("\n"))))
found = 0
directions = [
(-1, -1),
(-1, 0),
(-1, 1),
(0, -1),
(0, 1),
(1, -1),
(1, 0),
(1, 1),
]
word = "XMAS"
for y in range(grid.shape[0]):
for x in range(grid.shape[1]):
if grid[y, x] != "X":
continue
for dx, dy in directions:
end_x = x + 3 * dx
end_y = y + 3 * dy
if (
end_x < 0
or end_x >= grid.shape[1]
or end_y < 0
or end_y >= grid.shape[0]
):
continue
if all(
grid[y + i * dy, x + i * dx] == c for i, c in enumerate(word)
):
found += 1
return found
@classmethod
def part2(cls, input: str) -> int:
grid = numpy.array(list(map(list, input.strip().split("\n"))))
found = 0
magic = ord("M") ^ ord("S")
for y in range(1, grid.shape[0] - 1):
for x in range(1, grid.shape[1] - 1):
if grid[y, x] != "A":
continue
first_diag = ord(grid[y - 1, x - 1]) ^ ord(grid[y + 1, x + 1])
secnd_diag = ord(grid[y - 1, x + 1]) ^ ord(grid[y + 1, x - 1])
if first_diag == magic and secnd_diag == magic:
found += 1
return found

50
2024/src/aoc/days/day5.py Normal file
View File

@@ -0,0 +1,50 @@
import functools
from collections import defaultdict
from . import CombinedRunner
def parse_input(input: str) -> tuple[set[tuple[int, int]], list[list[int]]]:
first, second = input.strip().split("\n\n")
rules = {tuple(int(x) for x in line.split("|")) for line in first.split("\n")}
updates = [[int(x) for x in line.split(",")] for line in second.split("\n")]
return rules, updates
def is_correct(update: list[int], must_after: dict[int, set[int]]) -> bool:
forbidden = set()
for entry in update:
if entry in forbidden:
return False
forbidden |= must_after.get(entry, set())
return True
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> int:
rules, updates = parse_input(input)
must_after = defaultdict(set)
for before, after in rules:
must_after[after].add(before)
correct = 0
corrected = 0
key = functools.cmp_to_key(lambda a, b: -1 if (a, b) in rules else 1)
for update in updates:
if is_correct(update, must_after):
correct += update[len(update) // 2]
else:
update.sort(key=key)
corrected += update[len(update) // 2]
return correct, corrected

105
2024/src/aoc/days/day6.py Normal file
View File

@@ -0,0 +1,105 @@
import numpy
from . import SeparateRunner
def does_loop(
grid: numpy.array,
x: int,
y: int,
dx: int,
dy: int,
visited: set[tuple[int, int, int, int]],
) -> bool:
try:
while True:
while y + dy >= 0 and x + dx >= 0 and grid[y + dy, x + dx] == "#":
dx, dy = -dy, dx
x += dx
y += dy
if x < 0 or y < 0:
return False
pos = (x, y, dx, dy)
if pos in visited:
return True
else:
visited.add(pos)
except IndexError:
return False
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
grid = input.strip().split("\n")
for y, line in enumerate(grid):
if (x := line.find("^")) != -1:
break
dx = 0
dy = -1
visited = {(x, y)}
try:
while True:
nx = x + dx
ny = y + dy
if grid[ny][nx] == "#":
dx, dy = -dy, dx
else:
x, y = nx, ny
visited.add((x, y))
except IndexError:
pass
return len(visited)
@classmethod
def part2(cls, input: str) -> int:
grid = numpy.array(list(map(list, input.strip().split("\n"))))
y, x = numpy.where(grid == "^")
y = y[0]
x = x[0]
dx = 0
dy = -1
loops = 0
visited = {(x, y, dx, dy)}
tiles_visited = {(x, y)}
try:
while True:
while y + dy >= 0 and x + dx >= 0 and grid[y + dy, x + dx] == "#":
dx, dy = -dy, dx
nx = x + dx
ny = y + dy
if nx < 0 or ny < 0:
break
if (nx, ny) not in tiles_visited:
# check for a loop
grid[ny, nx] = "#"
if does_loop(grid, x, y, dx, dy, visited.copy()):
loops += 1
grid[ny, nx] = "L"
else:
grid[ny, nx] = "."
x, y = nx, ny
tiles_visited.add((x, y))
visited.add((x, y, dx, dy))
except IndexError:
pass
return loops

61
2024/src/aoc/days/day7.py Normal file
View File

@@ -0,0 +1,61 @@
from . import SeparateRunner
def parse_input(input: str) -> tuple[int, list[int]]:
result = []
for line in input.strip().split("\n"):
test, nums = line.split(": ")
result.append((int(test), list(map(int, nums.split(" ")))))
return result
def is_possible(target: int, nums: list[int]) -> bool:
if target == 0:
return not nums
if not nums or target < 0:
return False
tail = nums[-1]
remainder = nums[:-1]
return is_possible(target - tail, remainder) or (
target % tail == 0 and is_possible(target // tail, remainder)
)
def is_possible2(target: int, nums: list[int]) -> bool:
if target == 0:
return not nums
if not nums or target < 0:
return False
tail = nums[-1]
remainder = nums[:-1]
target_str = str(target)
tail_str = str(tail)
return (
is_possible2(target - tail, remainder)
or (target % tail == 0 and is_possible2(target // tail, remainder))
or (
target_str.endswith(tail_str)
and is_possible2(int(target_str[: -len(str(tail_str))]), remainder)
)
)
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
lines = parse_input(input)
return sum(target for target, nums in lines if is_possible(target, nums))
@classmethod
def part2(cls, input: str) -> int:
lines = parse_input(input)
return sum(target for target, nums in lines if is_possible2(target, nums))

60
2024/src/aoc/days/day8.py Normal file
View File

@@ -0,0 +1,60 @@
import itertools
import math
from collections import defaultdict
import numpy
from . import CombinedRunner
def simplify(vec: numpy.array) -> numpy.array:
if vec[0] == 0:
return numpy.array([0, 1])
elif vec[1] == 0:
return numpy.array([0, 1])
else:
div = math.gcd(*vec)
return vec // div
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> int:
grid = input.strip().split("\n")
height = len(grid)
width = len(grid[0])
antennae = defaultdict(list)
for y, line in enumerate(grid):
for x, c in enumerate(line):
if c != ".":
antennae[c].append(numpy.array([x, y]))
antinodes = set()
antinodes2 = set()
def in_bounds(node: numpy.array) -> bool:
return 0 <= node[0] < width and 0 <= node[1] < height
def add(node: numpy.array):
if in_bounds(node):
antinodes.add(tuple(node))
def walk(start: numpy.array, step: numpy.array):
for pos in itertools.count(start, step):
if in_bounds(pos):
antinodes2.add(tuple(pos))
else:
break
for values in antennae.values():
for a, b in itertools.combinations(values, 2):
add(2 * a - b)
add(2 * b - a)
step = simplify(b - a)
walk(b, step)
walk(a, -step)
return len(antinodes), len(antinodes2)

107
2024/src/aoc/days/day9.py Normal file
View File

@@ -0,0 +1,107 @@
import heapq
from . import SeparateRunner
def file_checksum(file_id: int, start: int, length: int) -> int:
return file_id * length * (2 * start + length - 1) // 2
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
files = []
empty = []
pos = 0
for c in input.strip():
val = int(c)
if len(files) == len(empty):
files.append((pos, val))
else:
empty.append((pos, val))
pos += val
checksum = 0
for start, length in empty:
while files and length > 0:
file_start, file_len = files.pop()
if file_start < start:
files.append((file_start, file_len))
break
file_id = len(files)
infill = min(file_len, length)
checksum += file_checksum(file_id, start, infill)
start += infill
if infill != file_len:
files.append((file_start, file_len - infill))
length -= infill
else:
continue
break
for file_id, (file_start, file_len) in enumerate(files):
checksum += file_checksum(file_id, file_start, file_len)
return checksum
@classmethod
def part2(cls, input: str) -> int:
files = []
empty = [[] for _ in range(10)]
pos = 0
is_file = True
for c in input.strip():
val = int(c)
if is_file:
files.append((pos, val))
is_file = False
else:
# No need for heappush, as we're appending values in order
empty[val].append(pos)
is_file = True
pos += val
checksum = 0
while files:
start, length = files.pop()
file_id = len(files)
best = None
best_heap = None
for i, heap in enumerate(empty[length:]):
if not heap or heap[0] > start:
continue
if best is None or best > heap[0]:
best = heap[0]
best_heap = i + length
if best is None:
# No room to move left, count score at current position
checksum += file_checksum(file_id, start, length)
else:
checksum += file_checksum(file_id, best, length)
heapq.heappop(empty[best_heap])
if length < best_heap:
remainder = best_heap - length
heapq.heappush(empty[remainder], best + length)
return checksum

8
2024/tests/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
import os
def get_data(day: int, sub: int | None = None) -> str:
basename = f"{day:02d}" if sub is None else f"{day:02d}.{sub}"
sample = os.path.dirname(__file__) + f"/samples/{basename}.txt"
with open(sample, mode="rt", encoding="utf-8") as f:
return f.read()

View File

@@ -0,0 +1,6 @@
3 4
4 3
2 5
1 3
3 9
3 3

View File

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

View File

@@ -0,0 +1,5 @@
..X...
.SAMX.
.A..A.
XMAS.S
.X....

View File

@@ -0,0 +1,10 @@
MMMSXXMASM
MSAMXMSMSA
AMXSXMAAMM
MSAMASMSMX
XMASAMXAMM
XXAMMXXAMA
SMSMSASXSS
SAXAMASAAA
MAMMMXMMMM
MXMXAXMASX

28
2024/tests/samples/05.txt Normal file
View File

@@ -0,0 +1,28 @@
47|53
97|13
97|61
97|47
75|29
61|13
75|53
29|13
97|29
53|29
61|53
97|53
61|29
47|13
75|47
97|75
47|61
75|61
47|29
75|13
53|13
75,47,61,53,29
97,61,53,29,13
75,29,13
75,97,47,61,53
61,13,29
97,13,75,29,47

10
2024/tests/samples/06.txt Normal file
View File

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

View File

@@ -0,0 +1,9 @@
190: 10 19
3267: 81 40 27
83: 17 5
156: 15 6
7290: 6 8 6 15
161011: 16 10 13
192: 17 8 14
21037: 9 7 18 13
292: 11 6 16 20

12
2024/tests/samples/08.txt Normal file
View File

@@ -0,0 +1,12 @@
............
........0...
.....0......
.......0....
....0.......
......A.....
............
............
........A...
.........A..
............
............

View File

@@ -0,0 +1,8 @@
89010123
78121874
87430965
96549874
45678903
32019012
01329801
10456732

View File

@@ -0,0 +1,4 @@
AAAA
BBCD
BBCC
EEEC

View File

@@ -0,0 +1,5 @@
OOOOO
OXOXO
OOOOO
OXOXO
OOOOO

View File

@@ -0,0 +1,10 @@
RRRRIICCFF
RRRRIICCCF
VVRRRCCFFF
VVRCCCJFFF
VVVVCJJCFE
VVIVCCJJEE
VVIIICJJEE
MIIIIIJJEE
MIIISIJEEE
MMMISSJEEE

15
2024/tests/samples/13.txt Normal file
View File

@@ -0,0 +1,15 @@
Button A: X+94, Y+34
Button B: X+22, Y+67
Prize: X=8400, Y=5400
Button A: X+26, Y+66
Button B: X+67, Y+21
Prize: X=12748, Y=12176
Button A: X+17, Y+86
Button B: X+84, Y+37
Prize: X=7870, Y=6450
Button A: X+69, Y+23
Button B: X+27, Y+71
Prize: X=18641, Y=10279

12
2024/tests/samples/14.txt Normal file
View File

@@ -0,0 +1,12 @@
p=0,4 v=3,-3
p=6,3 v=-1,-3
p=10,3 v=-1,2
p=2,0 v=2,-1
p=0,0 v=1,3
p=3,0 v=-2,-2
p=7,6 v=-1,-3
p=3,0 v=-1,-2
p=9,3 v=2,3
p=7,3 v=-1,2
p=2,4 v=2,-3
p=9,5 v=-3,-3

View File

@@ -0,0 +1,21 @@
##########
#..O..O.O#
#......O.#
#.OO..O.O#
#..O@..O.#
#O#..O...#
#O..O..O.#
#.OO.O.OO#
#....O...#
##########
<vv>^<v^>v>^vv^v>v<>v^v<v<^vv<<<^><<><>>v<vvv<>^v^>^<<<><<v<<<v^vv^v>^
vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<<v<^v>^<^^>>>^<v<v
><>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^<v>v^^<^^vv<
<<v<^>>^^^^>>>v^<>vvv^><v<<<>^^^vv^<vvv>^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
^><^><>>><>^^<<^^v>>><^<v>^<vv>>v>>>^v><>^v><<<<v>>v<v<v>vvv>^<><<>^><
^>><>^v<><^vvv<^^<><v<<<<<><^v<<<><<<^^<v<^^^><^>>^<v^><<<^>>^v<v^v<v^
>^>>^v>vv>^<<^v<>><<><<v<<v><>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
<><^^>^^^<><vvvvv^v<v<<>^v<v>v<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
^^>vv<^v^v<vv>^<><v<^v>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><<v>
v^^>>><<^^<>>^v^<v^vv<>v^<<>^<^v^v><^<<<><<^<v><v<>vv>>v><v^<vv<>v^<<^

View File

@@ -0,0 +1,10 @@
########
#..O.O.#
##@.O..#
#...O..#
#.#.O..#
#...O..#
#......#
########
<^^>>>vv<v>>v<<

View File

@@ -0,0 +1,9 @@
#######
#...#.#
#.....#
#..OO@#
#..O..#
#.....#
#######
<vv<<^^<<^^

View File

@@ -0,0 +1,15 @@
###############
#.......#....E#
#.#.###.#.###.#
#.....#.#...#.#
#.###.#####.#.#
#.#.#.......#.#
#.#.#####.###.#
#...........#.#
###.#.#####.#.#
#...#.....#.#.#
#.#.#.###.#.#.#
#.....#...#.#.#
#.###.#.#.#.#.#
#S..#.....#...#
###############

View File

@@ -0,0 +1,17 @@
#################
#...#...#...#..E#
#.#.#.#.#.#.#.#.#
#.#.#.#...#...#.#
#.#.#.#.###.#.#.#
#...#.#.#.....#.#
#.#.#.#.#.#####.#
#.#...#.#.#.....#
#.#.#####.#.###.#
#.#.#.......#...#
#.#.###.#####.###
#.#.#...#.....#.#
#.#.#.#####.###.#
#.#.#.........#.#
#.#.#.#########.#
#S#.............#
#################

View File

@@ -0,0 +1,5 @@
Register A: 729
Register B: 0
Register C: 0
Program: 0,1,5,4,3,0

View File

@@ -0,0 +1,5 @@
Register A: 2024
Register B: 0
Register C: 0
Program: 0,3,5,4,3,0

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

@@ -0,0 +1,25 @@
5,4
4,2
4,5
3,0
2,1
6,3
2,4
1,5
0,6
3,3
2,6
5,1
1,2
5,5
2,5
6,5
1,4
0,4
6,4
1,1
6,1
1,0
0,5
1,6
2,0

10
2024/tests/samples/19.txt Normal file
View File

@@ -0,0 +1,10 @@
r, wr, b, g, bwu, rb, gb, br
brwrr
bggr
gbbr
rrbgbr
ubwu
bwurrg
brgr
bbrgwb

15
2024/tests/samples/20.txt Normal file
View File

@@ -0,0 +1,15 @@
###############
#...#...#.....#
#.#.#.#.#.###.#
#S#...#.#.#...#
#######.#.#.###
#######.#.#...#
#######.#.###.#
###..E#...#...#
###.#######.###
#...###...#...#
#.#####.#.###.#
#.#...#.#.#...#
#.#.#.#.#.#.###
#...#...#...###
###############

View File

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

View File

@@ -0,0 +1,4 @@
1
10
100
2024

View File

@@ -0,0 +1,4 @@
1
2
3
2024

32
2024/tests/samples/23.txt Normal file
View File

@@ -0,0 +1,32 @@
kh-tc
qp-kh
de-cg
ka-co
yn-aq
qp-ub
cg-tb
vc-aq
tb-ka
wh-tc
yn-cg
kh-ub
ta-co
de-co
tc-td
tb-wq
wh-td
ta-ka
td-qp
aq-cg
wq-ub
ub-vc
de-ta
wq-aq
wq-vc
wh-yn
ka-de
kh-ta
co-tc
wh-qp
tb-vc
td-yn

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

11
2024/tests/test_day01.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day1 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(1)) == 11
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(1)) == 31

20
2024/tests/test_day02.py Normal file
View File

@@ -0,0 +1,20 @@
from aoc.days.day2 import DayRunner, is_savable
from . import get_data
def test_individual_samples() -> None:
assert is_savable("7 6 4 2 1")
assert not is_savable("1 2 7 8 9")
assert not is_savable("9 7 6 2 1")
assert is_savable("1 3 2 4 5")
assert is_savable("8 6 4 4 1")
assert is_savable("1 3 6 7 9")
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(2)) == 2
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(2)) == 4

12
2024/tests/test_day03.py Normal file
View File

@@ -0,0 +1,12 @@
from aoc.days.day3 import DayRunner
SAMPLE_1 = "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))"
SAMPLE_2 = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"
def test_sample_part1() -> None:
assert DayRunner.part1(SAMPLE_1) == 161
def test_sample_part2() -> None:
assert DayRunner.part2(SAMPLE_2) == 48

20
2024/tests/test_day04.py Normal file
View File

@@ -0,0 +1,20 @@
import pytest
from aoc.days.day4 import DayRunner
from . import get_data
@pytest.mark.parametrize(
"input,answer",
[
(get_data(4, 1), 4),
(get_data(4, 2), 18),
],
)
def test_sample_part1(input: str, answer: int) -> None:
assert DayRunner.part1(input) == answer
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(4, 2)) == 9

15
2024/tests/test_day05.py Normal file
View File

@@ -0,0 +1,15 @@
from aoc.days.day5 import DayRunner
from . import get_data
def test_sample_part1() -> None:
data = get_data(5)
assert DayRunner.part1(data) == 143
def test_sample_part2() -> None:
data = get_data(5)
assert DayRunner.part2(data) == 123

17
2024/tests/test_day06.py Normal file
View File

@@ -0,0 +1,17 @@
import os
from aoc.days.day6 import DayRunner
def get_data() -> str:
sample = os.path.dirname(__file__) + "/samples/06.txt"
with open(sample, mode="rt", encoding="utf-8") as f:
return f.read()
def test_sample_part1() -> None:
assert DayRunner.part1(get_data()) == 41
def test_sample_part2() -> None:
assert DayRunner.part2(get_data()) == 6

11
2024/tests/test_day07.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day7 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(7)) == 3749
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(7)) == 11387

11
2024/tests/test_day08.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day8 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(8)) == 14
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(8)) == 34

11
2024/tests/test_day09.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day9 import DayRunner
SAMPLE = "2333133121414131402"
def test_sample_part1() -> None:
assert DayRunner.part1(SAMPLE) == 1928
def test_sample_part2() -> None:
assert DayRunner.part2(SAMPLE) == 2858

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

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

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

@@ -0,0 +1,7 @@
from aoc.days.day11 import DayRunner
SAMPLE = "125 17"
def test_sample_part1() -> None:
assert DayRunner.part1(SAMPLE) == 55312

21
2024/tests/test_day12.py Normal file
View File

@@ -0,0 +1,21 @@
import pytest
from aoc.days.day12 import DayRunner
from . import get_data
@pytest.mark.parametrize(
"data,result",
[(get_data(12, 1), 140), (get_data(12, 2), 772), (get_data(12, 3), 1930)],
)
def test_sample_part1(data: str, result: int) -> None:
assert DayRunner.part1(data) == result
@pytest.mark.parametrize(
"data,result",
[(get_data(12, 1), 80), (get_data(12, 2), 436), (get_data(12, 3), 1206)],
)
def test_sample_part2(data: str, result: int) -> None:
assert DayRunner.part2(data) == result

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

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

Some files were not shown because too many files have changed in this diff Show More