mirror of
https://github.com/bertptrs/adventofcode.git
synced 2025-12-25 21:00:31 +01:00
Compare commits
8 Commits
be6c3d37ea
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| bc6f3dc8c6 | |||
| a8a1c85498 | |||
| ff8b8dac0c | |||
| 51ca5db42e | |||
| e60bfbf8c4 | |||
| 768a3ff10b | |||
| c3d3116f8a | |||
| 13592a60ee |
1
2025/day08/.gitignore
vendored
Normal file
1
2025/day08/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
solve
|
||||||
11
2025/day08/README.md
Normal file
11
2025/day08/README.md
Normal 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
3
2025/day08/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module bertptrs/solve/v2
|
||||||
|
|
||||||
|
go 1.25.5
|
||||||
20
2025/day08/sample.txt
Normal file
20
2025/day08/sample.txt
Normal 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
192
2025/day08/solve.go
Normal 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
1
2025/day09/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
14
2025/day09/README.md
Normal file
14
2025/day09/README.md
Normal 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
29
2025/day09/package-lock.json
generated
Normal 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
6
2025/day09/package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.10.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
2025/day09/sample.txt
Normal file
8
2025/day09/sample.txt
Normal 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
78
2025/day09/solve.ts
Executable 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
1
2025/day10/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
7
2025/day10/Cargo.toml
Normal file
7
2025/day10/Cargo.toml
Normal 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
3
2025/day10/sample.txt
Normal 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
141
2025/day10/src/main.rs
Normal 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
10
2025/day11/README.md
Normal 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
|
||||||
|
```
|
||||||
9
2025/day11/pyproject.toml
Normal file
9
2025/day11/pyproject.toml
Normal 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
10
2025/day11/sample.txt
Normal 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
14
2025/day11/sample2.txt
Normal 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
70
2025/day11/solve.py
Executable 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()
|
||||||
Reference in New Issue
Block a user