Ohh so it was possible to use floats for this! I was "worried" that it would lead to precision errors haha so I ended up "cheating" with BigInt (Golang) to make all the multiplications first and one division at the end
comments
Thank you! I love seeing well-documented solutions for problems that I'm struggling with, so I try to add comments for non-trivial parts of any code I post. :)
You even have comments, very nice!
Python
# pairwise helps picking consecutive values easier
# [A,B,C,D] -> [AB, BC, CD]
# combinations will give me combinations of elements in a sequence
# [A,B,C,D] -> [AB, AC, AD, BC, BD, CD]
from itertools import pairwise, combinations
# line will pass thorough the center if the points are on opposite ends,
# i.e., total points / 2 distance away
def part1(data: str, nail_count: int = 32):
opposite_dist = nail_count // 2
nodes = [int(n) for n in data.split(",")]
center_passes = 0
for a, b in pairwise(nodes):
if abs(a - b) == opposite_dist:
center_passes += 1
return center_passes
assert part1("1,5,2,6,8,4,1,7,3", 8) == 4
# BRUTEFORCE: take every two points and check if they intersect
def part2(data: str, nail_count: int = 32):
def intersects(s1, s2):
# expand the points
(x1, x2), (y1, y2) = s1, s2
# make sure x1 is the smaller nail and x2 is the bigger one
if x1 > x2:
x1, x2 = x2, x1
# there is an intersection if one point lies bwtween x1 and x2
# and the other lies outside it
if x1 < y1 < x2 and (y2 > x2 or y2 < x1):
return True
if x1 < y2 < x2 and (y1 > x2 or y1 < x1):
return True
return False
nodes = [int(n) for n in data.split(",")]
knots = 0
for s1, s2 in combinations(pairwise(nodes), 2):
if intersects(s1, s2):
knots += 1
return knots
assert part2("1,5,2,6,8,4,1,7,3,5,7,8,2", 8) == 21
from collections import defaultdict
# This is better than bruteforce
def part3(data: str, nail_count: int = 256):
connections = defaultdict(list)
nodes = [int(n) for n in data.split(",")]
# record all connections, both ways
for a, b in pairwise(nodes):
connections[a].append(b)
connections[b].append(a)
max_cuts = 0
for node_a in range(1, nail_count + 1):
cuts = 0
for node_b in range(node_a + 2, nail_count + 1):
# add new cuts for the node we just added between a and b
for other_node in connections[node_b - 1]:
if other_node > node_b or other_node < node_a:
cuts += 1
# also add any cuts for threads that go between node a and b
cuts += connections[node_a].count(node_b)
# remove cuts that were going from BETWEEN node_a and node_b-1 (prev node_b) to node_b
for other_node in connections[node_b]:
if node_a < other_node < node_b:
cuts -= 1
# also remove any cuts for threads that go between node a and b-1
cuts -= connections[node_a].count(node_b - 1)
max_cuts = max(max_cuts, cuts)
return max_cuts
assert part3("1,5,2,6,8,4,1,7,3,6", 8) == 7
Python
Used sliding window for part 3. The off-by-one difference between i and j tripped me up for a while.
def part1(data: str, mentor: str = 'A'):
mentors = 0
pairs = 0
for p in data:
if p == mentor:
mentors += 1
elif p == mentor.lower():
pairs += mentors
return pairs
assert part1("ABabACacBCbca") == 5
def part2(data: str):
all_pairs = 0
for mentor in 'ABC':
all_pairs += part1(data, mentor)
return all_pairs
assert part2("ABabACacBCbca") == 11
from collections import defaultdict
def part3(data: str, distance_limit: int = 1000, repeat: int = 1000):
n = len(data)
N = len(data) * repeat
pairs = 0
person_count = defaultdict(int)
curr = 0
# initialize the first window excluding the right boundary
for j in range(distance_limit):
person_count[data[j % n]] += 1
for curr in range(N):
# move left boundary (if applicable)
if (i := curr - distance_limit - 1) >= 0:
person_count[data[i % n]] -= 1
# move right boundary (if applicable)
if (j := curr + distance_limit) < N:
person_count[data[j % n]] += 1
# if mentee, record pairs
if data[curr % n].islower():
pairs += person_count[data[curr % n].upper()]
return pairs
assert (t := part3("AABCBABCABCabcabcABCCBAACBCa", 10, 1)) == 34, f"Expected 34 but got {t}"
assert (t := part3("AABCBABCABCabcabcABCCBAACBCa", 10, 2)) == 72, f"Expected 72 but got {t}"
assert (t := part3("AABCBABCABCabcabcABCCBAACBCa", 1000, 1000)) == 3442321, f"Expected 3442321 but got {t}"-
Rust
use std::collections::{HashMap, HashSet};
use itertools::Itertools;
fn explode_from_barrel(
map: &HashMap<(isize, isize), i8>,
mut exploded: HashSet<(isize, isize)>,
mut front: HashSet<(isize, isize)>,
) -> HashSet<(isize, isize)> {
while !front.is_empty() {
let mut next_front = HashSet::new();
for (i, j) in front.drain() {
exploded.insert((i, j));
let my_size = map[&(i, j)];
for (di, dj) in [(-1, 0), (0, -1), (0, 1), (1, 0)] {
let (ni, nj) = (i + di, j + dj);
if exploded.contains(&(ni, nj)) {
continue;
}
if let Some(neighour_size) = map.get(&(ni, nj))
&& *neighour_size <= my_size
{
next_front.insert((ni, nj));
}
}
}
front = next_front;
}
exploded
}
pub fn solve_part_1(input: &str) -> String {
let map: HashMap<(isize, isize), i8> = input
.lines()
.enumerate()
.flat_map(|(i, line)| {
line.chars()
.enumerate()
.map(move |(j, ch)| ((i as isize, j as isize), ch as i8 - '0' as i8))
})
.collect();
let exploded = HashSet::<(isize, isize)>::new();
let front: HashSet<(isize, isize)> = [(0, 0)].into_iter().collect();
explode_from_barrel(&map, exploded, front).len().to_string()
}
pub fn solve_part_2(input: &str) -> String {
let map: HashMap<(isize, isize), i8> = input
.lines()
.enumerate()
.flat_map(|(i, line)| {
line.chars()
.enumerate()
.map(move |(j, ch)| ((i as isize, j as isize), ch as i8 - '0' as i8))
})
.collect();
let exploded = HashSet::<(isize, isize)>::new();
let max_i = map.keys().map(|(i, _)| *i).max().unwrap();
let max_j = map.keys().map(|(_, j)| *j).max().unwrap();
let front: HashSet<(isize, isize)> = [(0, 0), (max_i, max_j)].into_iter().collect();
explode_from_barrel(&map, exploded, front).len().to_string()
}
pub fn solve_part_3(input: &str) -> String {
let map: HashMap<(isize, isize), i8> = input
.lines()
.enumerate()
.flat_map(|(i, line)| {
line.chars()
.enumerate()
.map(move |(j, ch)| ((i as isize, j as isize), ch as i8 - '0' as i8))
})
.collect();
let max_i = map.keys().map(|(i, _)| *i).max().unwrap();
let max_j = map.keys().map(|(_, j)| *j).max().unwrap();
let best_barrel = (0..=max_i)
.cartesian_product(0..=max_j)
.map(|(i, j)| {
((i, j), {
let exploded = HashSet::<(isize, isize)>::new();
let front: HashSet<(isize, isize)> = [(i, j)].into_iter().collect();
explode_from_barrel(&map, exploded, front)
})
})
.max_by_key(|(_, exploded)| exploded.len())
.unwrap();
let second_best_barrel = (0..=max_i)
.cartesian_product(0..=max_j)
.filter(|&(i, j)| !best_barrel.1.contains(&(i, j)))
.map(|(i, j)| {
((i, j), {
let exploded = best_barrel.1.clone();
let front: HashSet<(isize, isize)> = [(i, j)].into_iter().collect();
explode_from_barrel(&map, exploded, front)
})
})
.max_by_key(|(_, exploded)| exploded.len())
.unwrap();
let third_best_barrel = (0..=max_i)
.cartesian_product(0..=max_j)
.filter(|&(i, j)| !second_best_barrel.1.contains(&(i, j)))
.map(|(i, j)| {
((i, j), {
let exploded = second_best_barrel.1.clone();
let front: HashSet<(isize, isize)> = [(i, j)].into_iter().collect();
explode_from_barrel(&map, exploded, front)
})
})
.max_by_key(|(_, exploded)| exploded.len())
.unwrap();
let exploded = HashSet::<(isize, isize)>::new();
let front: HashSet<(isize, isize)> = [best_barrel.0, second_best_barrel.0, third_best_barrel.0]
.into_iter()
.collect();
explode_from_barrel(&map, exploded, front).len().to_string()
}
Rust
pub fn solve_part_1(input: &str) -> String {
let numbers = input
.lines()
.map(|l| l.parse().unwrap())
.collect::<Vec<i64>>();
let offset = 2025 % (numbers.len() + 1);
if offset == 0 {
1
} else if offset > numbers.len().div_ceil(2) {
numbers[(numbers.len() - offset) * 2 + 1]
} else {
numbers[(offset - 1) * 2]
}
.to_string()
}
fn find_number(ranges: &[(i64, i64)], mut offset: i64, counterclockwise: bool) -> i64 {
for (from, to) in ranges {
let segment_size = (to - from) + 1;
if offset >= segment_size {
offset -= segment_size;
continue;
}
return if counterclockwise {
to - offset
} else {
from + offset
};
}
panic!("find_number gave up and died");
}
fn solve_part_2_with_turns(input: &str, turns: i64) -> String {
let ranges = input
.lines()
.map(|l| {
let (l, r) = l.split_once("-").unwrap();
(l.parse().unwrap(), r.parse().unwrap())
})
.collect::<Vec<(i64, i64)>>();
let mut clockwise_length = 0;
let mut clockwise_ranges = vec![];
let mut counterclockwise_length = 0;
let mut counterclockwise_ranges = vec![];
for (i, (from, to)) in ranges.into_iter().enumerate() {
if i % 2 == 0 {
clockwise_length += to - from + 1;
clockwise_ranges.push((from, to));
} else {
counterclockwise_length += to - from + 1;
counterclockwise_ranges.push((from, to));
}
}
counterclockwise_ranges.reverse();
let offset = turns % (clockwise_length + counterclockwise_length + 1);
if offset == 0 {
1
} else if offset > clockwise_length {
find_number(
&counterclockwise_ranges,
offset - clockwise_length - 1,
true,
)
} else {
find_number(&clockwise_ranges, offset - 1, false)
}
.to_string()
}
pub fn solve_part_2(input: &str) -> String {
solve_part_2_with_turns(input, 20252025)
}
pub fn solve_part_3(input: &str) -> String {
solve_part_2_with_turns(input, 202520252025)
}
Rust
Finally caught up with the puzzles.
use std::collections::{HashMap, HashSet};
use itertools::Itertools;
pub fn solve_part_1(input: &str) -> String {
let mut barrels: HashSet<(isize, isize)> = input
.lines()
.enumerate()
.flat_map(|(i, line)| {
line.chars()
.enumerate()
.filter(|&(_, ch)| ch == '#')
.map(move |(j, _)| (i as isize, j as isize))
})
.collect();
let max_i = barrels.iter().map(|(i, _)| *i).max().unwrap();
let max_j = barrels.iter().map(|(_, j)| *j).max().unwrap();
let mut total_active = 0;
for _ in 0..10 {
let mut next_barrels = HashSet::new();
for i in 0..=max_i {
for j in 0..=max_j {
let active_neighbours = [(-1, -1), (1, 1), (-1, 1), (1, -1)]
.iter()
.filter(|(di, dj)| barrels.contains(&(i + di, j + dj)))
.count();
let will_be_active = if barrels.contains(&(i, j)) {
active_neighbours % 2 == 1
} else {
active_neighbours % 2 == 0
};
if will_be_active {
next_barrels.insert((i, j));
}
}
}
barrels = next_barrels;
total_active += barrels.len();
}
total_active.to_string()
}
pub fn solve_part_2(input: &str) -> String {
let mut barrels: HashSet<(isize, isize)> = input
.lines()
.enumerate()
.flat_map(|(i, line)| {
line.chars()
.enumerate()
.filter(|&(_, ch)| ch == '#')
.map(move |(j, _)| (i as isize, j as isize))
})
.collect();
let max_i = barrels.iter().map(|(i, _)| *i).max().unwrap();
let max_j = barrels.iter().map(|(_, j)| *j).max().unwrap();
let mut total_active = 0;
for _ in 0..2025 {
let mut next_barrels = HashSet::new();
for i in 0..=max_i {
for j in 0..=max_j {
let active_neighbours = [(-1, -1), (1, 1), (-1, 1), (1, -1)]
.iter()
.filter(|(di, dj)| barrels.contains(&(i + di, j + dj)))
.count();
let will_be_active = if barrels.contains(&(i, j)) {
active_neighbours % 2 == 1
} else {
active_neighbours % 2 == 0
};
if will_be_active {
next_barrels.insert((i, j));
}
}
}
barrels = next_barrels;
total_active += barrels.len();
}
total_active.to_string()
}
pub fn solve_part_3(input: &str) -> String {
let pattern: HashSet<(isize, isize)> = input
.lines()
.enumerate()
.flat_map(|(i, line)| {
line.chars()
.enumerate()
.filter(|&(_, ch)| ch == '#')
.map(move |(j, _)| (i as isize, j as isize))
})
.collect();
let pattern_max_i = pattern.iter().map(|(i, _)| *i).max().unwrap();
let pattern_max_j = pattern.iter().map(|(_, j)| *j).max().unwrap();
assert_eq!(7, pattern_max_i);
assert_eq!(7, pattern_max_j);
let mut barrels: HashSet<(isize, isize)> = HashSet::new();
let max_i = 33;
let max_j = 33;
let mut state_map = HashMap::<Vec<(isize, isize)>, usize>::new();
state_map.insert(vec![], 0);
let mut current_round = 0;
let rounds_to_simulate = 1000000000;
let mut active_tiles_after_rounds = vec![0];
while current_round < rounds_to_simulate {
let mut next_barrels = HashSet::new();
for i in 0..=max_i {
for j in 0..=max_j {
let active_neighbours = [(-1, -1), (1, 1), (-1, 1), (1, -1)]
.iter()
.filter(|(di, dj)| barrels.contains(&(i + di, j + dj)))
.count();
let will_be_active = if barrels.contains(&(i, j)) {
active_neighbours % 2 == 1
} else {
active_neighbours % 2 == 0
};
if will_be_active {
next_barrels.insert((i, j));
}
}
}
barrels = next_barrels;
current_round += 1;
let state_key: Vec<_> = barrels.iter().copied().collect();
if let Some(&seen_before_after_round) = state_map.get(&state_key) {
let loop_length = current_round - seen_before_after_round;
let loops_remaining = (rounds_to_simulate - current_round) / loop_length;
let iterations_remaining =
rounds_to_simulate - current_round - (loops_remaining * loop_length);
return (active_tiles_after_rounds[0..current_round]
.iter()
.sum::<usize>()
+ active_tiles_after_rounds[seen_before_after_round..current_round]
.iter()
.sum::<usize>()
* loops_remaining
+ active_tiles_after_rounds
[seen_before_after_round..(seen_before_after_round + iterations_remaining)]
.iter()
.sum::<usize>())
.to_string();
}
state_map.insert(state_key, current_round);
let does_pattern_match = (13..=20)
.cartesian_product(13..=20)
.all(|(i, j)| barrels.contains(&(i, j)) == pattern.contains(&(i - 13, j - 13)));
active_tiles_after_rounds.push(if does_pattern_match { barrels.len() } else { 0 });
}
active_tiles_after_rounds.iter().sum::<usize>().to_string()
}
Python
# snipped read_input implementation
data = read_input("input_p3.txt")
# part 1 and 2 can be solved using a calculator
# with only the first and last gears
def part3(data):
shafts = data.splitlines()
rotations = 100 * int(shafts[0])
for shaft in shafts[1:-1]:
gear1, gear2 = [int(g) for g in shaft.split('|')]
rotations *= gear2 / gear1
rotations /= int(shafts[-1])
return int(rotations)
assert part3("""5
7|21
18|36
27|27
10|50
10|50
11""") == 6818
print(part3(data))
Rust
I hate when you need to look at the data to solve a simpler subclass of the problem.
use num::Integer;
fn one_round(columns: &mut [i64], forward: bool) -> bool {
let mut modified = false;
for i in 1..columns.len() {
if forward {
if columns[i - 1] > columns[i] {
columns[i - 1] -= 1;
columns[i] += 1;
modified = true;
}
} else if columns[i - 1] < columns[i] {
columns[i - 1] += 1;
columns[i] -= 1;
modified = true;
}
}
modified
}
pub fn solve_part_1(input: &str) -> String {
let mut columns = input
.lines()
.map(|l| l.parse().unwrap())
.collect::<Vec<i64>>();
let mut rounds_remaining = 10;
while rounds_remaining > 0 {
if one_round(&mut columns, true) {
rounds_remaining -= 1;
} else {
break;
}
}
while rounds_remaining > 0 {
if one_round(&mut columns, false) {
rounds_remaining -= 1;
} else {
break;
}
}
columns
.iter()
.enumerate()
.map(|(column_idx, count)| (column_idx + 1) as i64 * *count)
.sum::<i64>()
.to_string()
}
pub fn solve_part_2(input: &str) -> String {
let mut columns = input
.lines()
.map(|l| l.parse().unwrap())
.collect::<Vec<i64>>();
let mut total_rounds = 0;
loop {
if one_round(&mut columns, true) {
total_rounds += 1;
} else {
break;
}
}
loop {
if one_round(&mut columns, false) {
total_rounds += 1;
} else {
break;
}
}
total_rounds.to_string()
}
pub fn solve_part_3(input: &str) -> String {
let mut columns = input
.lines()
.map(|l| l.parse().unwrap())
.collect::<Vec<i64>>();
let invariant_sum = columns.iter().sum::<i64>();
let mut total_rounds = 0i64;
loop {
let first_gap = (0..columns.len() - 1).find(|&i| columns[i].abs_diff(columns[i + 1]) > 1);
let last_gap = (0..columns.len() - 1)
.filter(|&i| columns[i].abs_diff(columns[i + 1]) > 1)
.next_back();
if let (Some(first_gap), Some(last_gap)) = (first_gap, last_gap)
&& (last_gap > first_gap)
{
let amount_to_fill: i64 = columns
.iter()
.take(first_gap + 1)
.map(|&x| columns[first_gap + 1] - x)
.sum();
let amount_available: i64 = columns
.iter()
.skip(last_gap + 1)
.map(|&x| x - columns[last_gap])
.sum();
let amount_to_transfer = amount_to_fill.min(amount_available);
let new_amount_left: i64 =
columns.iter().take(first_gap + 1).sum::<i64>() + amount_to_transfer;
let (left_base, remainder) = new_amount_left.div_rem(&((first_gap + 1) as i64));
for (i, new_value) in columns.iter_mut().enumerate().take(first_gap + 1) {
*new_value = left_base
+ if first_gap - i < remainder as usize {
1
} else {
0
};
}
let new_amount_right: i64 =
columns.iter().skip(last_gap + 1).sum::<i64>() - amount_to_transfer;
let (right_base, remainder) =
new_amount_right.div_rem(&((columns.len() - last_gap - 1) as i64));
for i in last_gap + 1..columns.len() {
columns[i] = right_base
+ if columns.len() - i <= remainder as usize {
1
} else {
0
};
}
assert_eq!(invariant_sum, columns.iter().sum::<i64>());
total_rounds += amount_to_transfer;
} else {
break;
}
}
loop {
if one_round(&mut columns, false) {
total_rounds += 1;
} else {
break;
}
}
total_rounds.to_string()
}
Scheme/Guile
I was stuck on part 3 for a while, for not taking account that a child scale that I'm evaluating may already be a parent scale in some group.
(import (rnrs io ports (6))
(srfi srfi-1))
#!curly-infix
(define (parse-file file-name)
(let* ((lines (string-split (string-trim-both (call-with-input-file file-name get-string-all)) #\newline))
(split-lines (map (lambda (l) (string-split l #\:)) lines))
(parsed-lines (map (lambda (l) (cons (string->number (car l)) (string->list (cadr l)))) split-lines)))
parsed-lines))
(define (child-score child p1 p2 p1-sim p2-sim)
(if (and-map null? (list child p1 p2))
(* p1-sim p2-sim)
(let ((matches-p1 (eq? (car child) (car p1)))
(matches-p2 (eq? (car child) (car p2))))
(cond
((not (or matches-p1 matches-p2)) #f)
(else (child-score (cdr child) (cdr p1) (cdr p2) (+ p1-sim (if matches-p1 1 0)) (+ p2-sim (if matches-p2 1 0))))))))
(let ((dna-lines (parse-file "notes/everybody_codes_e2025_q09_p1.txt")))
(format #t "P1 Answer: ~a\n\n" (or
(child-score (cdar dna-lines) (cdadr dna-lines) (cdaddr dna-lines) 0 0)
(child-score (cdadr dna-lines) (cdar dna-lines) (cdaddr dna-lines) 0 0)
(child-score (cdaddr dna-lines) (cdadr dna-lines) (cdar dna-lines) 0 0))))
(let ((dna-lines (list->vector (parse-file "notes/everybody_codes_e2025_q09_p2.txt"))))
(let loop ((child 0) (total-sim 0))
(if {child < (vector-length dna-lines)}
(loop (1+ child) (+ total-sim (let loop ((i 0))
(cond
((eq? i child) (loop (1+ i)))
({i >= {(vector-length dna-lines) - 1}} 0)
(else
(or
(let loop ((j (1+ i)))
(cond
((eq? j child) (loop (1+ j)))
({j >= (vector-length dna-lines)} #f)
(else (let ((res (child-score
(cdr (vector-ref dna-lines child))
(cdr (vector-ref dna-lines i))
(cdr (vector-ref dna-lines j)) 0 0)))
(or res (loop (1+ j)))))))
(loop (1+ i))))))))
(format #t "P2 Answer: ~a\n\n" total-sim))))
(define (init-id-to-group dna-lines)
(let ((table (make-hash-table)))
(let loop ((i 0))
(if {i < (vector-length dna-lines)}
(let ((id (car (vector-ref dna-lines i))))
(hash-set! table id id)
(loop (1+ i)))
table))))
(define (init-group-to-ids dna-lines)
(let ((table (make-hash-table)))
(let loop ((i 0))
(if {i < (vector-length dna-lines)}
(let ((id (car (vector-ref dna-lines i))))
(hash-set! table id (list id))
(loop (1+ i)))
table))))
(let ((dna-lines (list->vector (parse-file "notes/everybody_codes_e2025_q09_p3.txt"))))
(let ((id-to-group (init-id-to-group dna-lines)) (group-to-ids (init-group-to-ids dna-lines)))
(let child-loop ((child 0))
(if {child < (vector-length dna-lines)}
(let i-loop ((i 0))
(cond
((eq? i child) (i-loop (1+ i)))
({i >= {(vector-length dna-lines) - 1}} (child-loop (1+ child)))
(else
(let j-loop ((j (1+ i)))
(cond
((eq? j child) (j-loop (1+ j)))
({j >= (vector-length dna-lines)} (i-loop (1+ i)))
(else (let* ((cl (vector-ref dna-lines child))
(pil (vector-ref dna-lines i))
(pjl (vector-ref dna-lines j))
(res (child-score (cdr cl) (cdr pil) (cdr pjl) 0 0)))
(if res
(let* ((i-group (hash-ref id-to-group (car pil)))
(j-group (hash-ref id-to-group (car pjl)))
(child-group (hash-ref id-to-group (car cl)))
(i-group-ids (hash-ref group-to-ids i-group))
(j-group-ids (hash-ref group-to-ids j-group))
(child-group-ids (hash-ref group-to-ids child-group))
(new-group-ids (delete-duplicates (append child-group-ids (or i-group-ids '()) (or j-group-ids '())))))
(map (lambda (id) (hash-set! id-to-group id child-group)) new-group-ids)
(hash-remove! group-to-ids i-group)
(hash-remove! group-to-ids j-group)
(hash-set! group-to-ids child-group new-group-ids)
(child-loop (1+ child)))
(j-loop (1+ j))))))))))
(format #t "P3 Answer: ~a\n\n" (cdr (hash-fold
(lambda (_ id-list prior)
(let ((group-size (length id-list))
(group-sum (apply + id-list)))
(if {group-size > (car prior)}
(cons group-size group-sum)
prior)))
(cons 0 0)
group-to-ids)))))))
Scheme/Guile
Takes about 5 seconds.
(import (rnrs io ports (6))
(srfi srfi-1))
#!curly-infix
(define (parse-file file-name)
(let ((sequence (map string->number (string-split (string-trim-both (call-with-input-file file-name get-string-all)) #\,))))
(zip sequence (cdr sequence))))
(let loop ((sequence (parse-file "notes/everybody_codes_e2025_q08_p1.txt")) (count 0))
(if (null? sequence)
(format #t "P1 Answer: ~a\n\n" count)
(loop (cdr sequence) (+ count (if (and last (eq? (modulo (- (cadar sequence) (caar sequence)) 32) 16)) 1 0)))))
(define (crosses-over? a b)
(let ((a1 (car a))
(a2 (cadr a))
(b1 (car b))
(b2 (cadr b)))
(let ((a2 (modulo {a2 - a1} 256))
(b1 (modulo {b1 - a1} 256))
(b2 (modulo {b2 - a1} 256)))
(and (not (eq? b1 0)) (not (eq? b2 0))
(or
(and {b1 < a2} {b2 > a2})
(and {b1 > a2} {b2 < a2}))))))
(define (count-cross-overs sequence a)
(let loop ((sequence sequence) (count 0))
(if (null? sequence)
count
(loop (cdr sequence) (+ count (if (crosses-over? (car sequence) a) 1 0))))))
(let loop ((sequence (parse-file "notes/everybody_codes_e2025_q08_p2.txt")) (passed '()) (count 0))
(if (null? sequence)
(format #t "P2 Answer: ~a\n\n" count)
(loop (cdr sequence) (cons (car sequence) passed) (+ count (count-cross-overs passed (car sequence))))))
(let ((sequence (parse-file "notes/everybody_codes_e2025_q08_p3.txt")))
(let loop ((i 1) (greatest 0))
(if {i > 256}
(format #t "P3 Answer: ~a\n\n" greatest)
(loop (1+ i) (max greatest (let loop ((j i) (greatest 0))
(if {j > 256}
greatest
(loop (1+ j) (max greatest (count-cross-overs sequence (list i j)))))))))))
Scheme/Guile
You could probably build a (letter, length) => combination-count mapping pretty quickly for part 3, but dealing with overlap in the input elements seems like a pain if handled this way.
(import (rnrs io ports (6)))
#!curly-infix
(define (parse-file file-name) (let*
((lines (string-split (string-trim-both (call-with-input-file file-name get-string-all)) #\newline))
(names (string-split (car lines) #\,))
(rule-lines (cddr lines))
(rules (map (lambda (l) (let*
((sides (string-split l #\space))
(r-left (string-ref (car sides) 0))
(r-right (caddr sides)))
(cons r-left (map (lambda (x) (string-ref x 0)) (string-split r-right #\,))))) rule-lines)))
(cons names rules)))
(define (check-name rules name)
(if (eq? 1 (string-length name))
#t
(let* ((letter (string-ref name 0))
(right (assq-ref rules letter))
(next-letter (string-ref name 1)))
(if (memq next-letter right)
(check-name rules (substring/read-only name 1))
#f))))
(let* ((parsed (parse-file "notes/everybody_codes_e2025_q07_p1.txt"))
(names (car parsed))
(rules (cdr parsed)))
(let loop ((names names))
(let* ((name (car names)) (name-matches (check-name rules name)))
(if name-matches
(format #t "P1 Answer: ~a\n\n" name)
(loop (cdr names))))))
(let* ((parsed (parse-file "notes/everybody_codes_e2025_q07_p2.txt"))
(names (car parsed))
(rules (cdr parsed)))
(let loop ((i 1) (names names) (name-sum 0))
(if (null? names)
(format #t "P2 Answer: ~a\n\n" name-sum)
(let* ((name (car names)) (name-matches (check-name rules name)))
(loop (1+ i) (cdr names) (+ name-sum (if name-matches i 0)))))))
(define discovered-prefixes (make-hash-table))
(define (count-prefixes rules name)
(if (hash-ref discovered-prefixes name)
0
(begin
(hash-set! discovered-prefixes name #t)
(if {(string-length name) >= 11}
1
(+
(apply + (map
(lambda (c) (count-prefixes rules (string-append name (string c))))
(or (assq-ref rules (string-ref name (1- (string-length name)))) '())))
(if {(string-length name) >= 7} 1 0))))))
(let* ((parsed (parse-file "notes/everybody_codes_e2025_q07_p3.txt"))
(names (car parsed))
(rules (cdr parsed)))
(let ((name-count (apply + (map (lambda (name) (count-prefixes rules name)) (filter (lambda (name) (check-name rules name)) names)))))
(format #t "P3 Answer: ~a\n\n" name-count)))
Rust
use std::collections::{BTreeSet, HashMap, HashSet};
use itertools::Itertools;
pub fn solve_part_1(input: &str) -> String {
let board: Vec<Vec<_>> = input.lines().map(|l| l.chars().collect()).collect();
let mut front: HashSet<_> = (0usize..board.len())
.cartesian_product(0usize..board[0].len())
.filter(|&(i, j)| board[i][j] == 'D')
.collect();
let mut visited = HashSet::new();
let knight_moves: [(isize, isize); 8] = [
(2, 1),
(2, -1),
(-2, -1),
(-2, 1),
(1, 2),
(1, -2),
(-1, -2),
(-1, 2),
];
for _ in 0..=4 {
let mut next_front = HashSet::new();
for (i, j) in front.drain() {
for (di, dj) in knight_moves {
let (ni, nj) = (i.wrapping_add_signed(di), j.wrapping_add_signed(dj));
if ni >= board.len() || nj >= board[0].len() {
continue;
}
if visited.contains(&(ni, nj)) {
continue;
}
next_front.insert((ni, nj));
}
visited.insert((i, j));
}
front = next_front;
}
visited
.drain()
.filter(|&(i, j)| board[i][j] == 'S')
.count()
.to_string()
}
fn solve_part_2_with_turns(input: &str, turns: usize) -> String {
let board: Vec<Vec<_>> = input.lines().map(|l| l.chars().collect()).collect();
let mut front: HashSet<_> = (0usize..board.len())
.cartesian_product(0usize..board[0].len())
.filter(|&(i, j)| board[i][j] == 'D')
.collect();
let knight_moves: [(isize, isize); 8] = [
(2, 1),
(2, -1),
(-2, -1),
(-2, 1),
(1, 2),
(1, -2),
(-1, -2),
(-1, 2),
];
let mut eaten_sheep = HashSet::new();
for turn in 0..=turns {
let mut next_front = HashSet::new();
for (i, j) in front.drain() {
for (di, dj) in knight_moves {
let (ni, nj) = (i.wrapping_add_signed(di), j.wrapping_add_signed(dj));
if ni >= board.len() || nj >= board[0].len() {
continue;
}
next_front.insert((ni, nj));
}
if board[i][j] != '#' {
if let Some(sheep_i) = (i + 1).checked_sub(turn)
&& board[sheep_i][j] == 'S'
{
eaten_sheep.insert((sheep_i, j));
}
if let Some(sheep_i) = i.checked_sub(turn)
&& turn != 0
&& board[sheep_i][j] == 'S'
{
eaten_sheep.insert((sheep_i, j));
}
}
}
front = next_front;
}
eaten_sheep.len().to_string()
}
pub fn solve_part_2(input: &str) -> String {
solve_part_2_with_turns(input, 20)
}
type VeryComplexType = HashMap<(usize, usize, usize, Vec<(usize, usize)>), usize>;
fn count_winning_sequences(
turn: usize,
dragon: (usize, usize),
hiding_places: &HashSet<(usize, usize)>,
sheep: BTreeSet<(usize, usize)>,
height: usize,
width: usize,
cache: &mut VeryComplexType,
) -> usize {
if sheep.is_empty() {
return 1;
}
let cache_key = (
turn % 2,
dragon.0,
dragon.1,
sheep.iter().cloned().collect(),
);
if let Some(result) = cache.get(&cache_key) {
return *result;
}
if turn % 2 == 1 {
let knight_moves: [(isize, isize); 8] = [
(2, 1),
(2, -1),
(-2, -1),
(-2, 1),
(1, 2),
(1, -2),
(-1, -2),
(-1, 2),
];
let (i, j) = dragon;
let mut total = 0;
for (di, dj) in knight_moves {
let (ni, nj) = (i.wrapping_add_signed(di), j.wrapping_add_signed(dj));
if ni >= height || nj >= width {
continue;
}
if !hiding_places.contains(&(ni, nj)) && sheep.contains(&(ni, nj)) {
let mut new_sheep = sheep.clone();
new_sheep.remove(&(ni, nj));
total += count_winning_sequences(
turn + 1,
(ni, nj),
hiding_places,
new_sheep,
height,
width,
cache,
);
} else {
total += count_winning_sequences(
turn + 1,
(ni, nj),
hiding_places,
sheep.clone(),
height,
width,
cache,
);
}
}
cache.insert(cache_key, total);
total
} else {
let mut sheep_moves_available = false;
let mut total = 0;
for &(i, j) in sheep.iter() {
if dragon == (i + 1, j) && !hiding_places.contains(&(i + 1, j)) {
continue;
}
sheep_moves_available = true;
if i == (height - 1) {
continue;
}
let mut new_sheep = sheep.clone();
new_sheep.remove(&(i, j));
new_sheep.insert((i + 1, j));
total += count_winning_sequences(
turn + 1,
dragon,
hiding_places,
new_sheep,
height,
width,
cache,
);
}
if !sheep_moves_available {
return count_winning_sequences(
turn + 1,
dragon,
hiding_places,
sheep,
height,
width,
cache,
);
}
cache.insert(cache_key, total);
total
}
}
pub fn solve_part_3(input: &str) -> String {
let board: Vec<Vec<_>> = input.lines().map(|l| l.chars().collect()).collect();
let dragon = (0usize..board.len())
.cartesian_product(0usize..board[0].len())
.filter(|&(i, j)| board[i][j] == 'D')
.exactly_one()
.unwrap();
let sheep = (0usize..board.len())
.cartesian_product(0usize..board[0].len())
.filter(|&(i, j)| board[i][j] == 'S')
.collect::<BTreeSet<_>>();
let hiding_places = (0usize..board.len())
.cartesian_product(0usize..board[0].len())
.filter(|&(i, j)| board[i][j] == '#')
.collect::<HashSet<_>>();
let mut cache = HashMap::new();
count_winning_sequences(
0,
dragon,
&hiding_places,
sheep,
board.len(),
board[0].len(),
&mut cache,
)
.to_string()
}
Rust
use std::collections::HashMap;
use bit_set::BitSet;
use itertools::{Itertools, izip};
type BitSetBase = u32;
fn is_child(child: &str, parent1: &str, parent2: &str) -> bool {
izip!(child.chars(), parent1.chars(), parent2.chars()).all(|(c, a, b)| c == a || c == b)
}
fn similarity(a: &str, b: &str) -> usize {
izip!(a.chars(), b.chars()).filter(|(a, b)| a == b).count()
}
fn unequality_bitset(a: &str, b: &str) -> BitSet<BitSetBase> {
izip!(a.chars(), b.chars())
.enumerate()
.filter_map(|(i, (a_ch, b_ch))| if a_ch == b_ch { None } else { Some(i) })
.collect()
}
pub fn solve_part_1(input: &str) -> String {
let dnas = input
.lines()
.map(|l| {
let (_, dna) = l.split_once(":").unwrap();
dna
})
.collect::<Vec<_>>();
let (child, parenta, parentb) = if is_child(dnas[0], dnas[1], dnas[2]) {
(dnas[0], dnas[1], dnas[2])
} else if is_child(dnas[1], dnas[0], dnas[2]) {
(dnas[1], dnas[0], dnas[2])
} else {
(dnas[2], dnas[0], dnas[1])
};
(similarity(child, parenta) * similarity(child, parentb)).to_string()
}
pub fn solve_part_2(input: &str) -> String {
let dnas = input
.lines()
.map(|l| {
let (_, dna) = l.split_once(":").unwrap();
dna
})
.collect::<Vec<_>>();
let unequalities = dnas
.iter()
.enumerate()
.cartesian_product(dnas.iter().enumerate())
.map(|((a_id, a), (b_id, b))| ((a_id, b_id), unequality_bitset(a, b)))
.collect::<HashMap<_, _>>();
let mut total_similarities = 0;
'outer: for (child_id, &child_string) in dnas.iter().enumerate() {
for (parent_a_id, &parent_a_string) in dnas.iter().enumerate() {
if parent_a_id == child_id {
continue;
}
for (parent_b_id, &parent_b_string) in dnas.iter().enumerate() {
if parent_b_id == child_id || parent_b_id == parent_a_id {
continue;
}
if unequalities[&(child_id, parent_a_id)]
.intersection(&unequalities[&(child_id, parent_b_id)])
.count()
== 0
{
total_similarities += similarity(child_string, parent_a_string)
* similarity(child_string, parent_b_string);
continue 'outer;
}
}
}
}
total_similarities.to_string()
}
pub fn solve_part_3(input: &str) -> String {
let dnas = input
.lines()
.map(|l| {
let (_, dna) = l.split_once(":").unwrap();
dna
})
.collect::<Vec<_>>();
let unequalities = dnas
.iter()
.enumerate()
.cartesian_product(dnas.iter().enumerate())
.map(|((a_id, a), (b_id, b))| ((a_id, b_id), unequality_bitset(a, b)))
.collect::<HashMap<_, _>>();
let mut parents: HashMap<usize, (usize, usize)> = HashMap::new();
'outer: for (child_id, _) in dnas.iter().enumerate() {
for (parent_a_id, _) in dnas.iter().enumerate() {
if parent_a_id == child_id {
continue;
}
for (parent_b_id, _) in dnas.iter().enumerate() {
if parent_b_id == child_id || parent_b_id == parent_a_id {
continue;
}
if unequalities[&(child_id, parent_a_id)]
.intersection(&unequalities[&(child_id, parent_b_id)])
.count()
== 0
{
parents.insert(child_id, (parent_a_id, parent_b_id));
continue 'outer;
}
}
}
}
let mut family_id = (0usize..dnas.len()).collect::<Vec<_>>();
for (child, (parent_a, parent_b)) in parents.drain() {
let destination_family_id = family_id[child];
let merge_families = (family_id[parent_a], family_id[parent_b]);
for candidate_family in family_id.iter_mut() {
if *candidate_family == merge_families.0 || *candidate_family == merge_families.1 {
*candidate_family = destination_family_id;
}
}
}
let families = family_id
.iter()
.enumerate()
.map(|(member_idx, &family_idx)| (family_idx, member_idx))
.into_group_map();
families
.values()
.max_by_key(|f| f.len())
.unwrap()
.iter()
.map(|member_id| member_id + 1)
.sum::<usize>()
.to_string()
}
I don't think there's such a thing as a "spirit of the question", but you're free to set your own challenges of course :)
Scheme/Guile
Part 3 was a fun little challenge.
(import (rnrs io ports (6)))
#!curly-infix
(define (parse-file file-name) (string-trim-both (call-with-input-file file-name get-string-all)))
(let* ((line (parse-file "notes/everybody_codes_e2025_q06_p1.txt"))
(line-length (string-length line)))
(let loop ((i 0) (knight-count 0) (mentor-count 0))
(if {i < line-length}
(let ((letter (string-ref line i)))
(loop (1+ i) (+ knight-count (if (eq? letter #\A) 1 0)) (+ mentor-count (if (eq? letter #\a) knight-count 0))))
(format #t "P1 Answer: ~a\n\n" mentor-count))))
(let* ((line (parse-file "notes/everybody_codes_e2025_q06_p2.txt"))
(line-length (string-length line)))
(let loop ((i 0) (knight-counts '()) (mentor-count 0))
(if {i < line-length}
(let ((letter (string-ref line i)))
(loop
(1+ i)
(if (char-upper-case? letter) (assq-set! knight-counts letter (1+ (or (assq-ref knight-counts letter) 0))) knight-counts)
(+ mentor-count (if (char-lower-case? letter) (or (assq-ref knight-counts (char-upcase letter)) 0) 0))))
(format #t "P2 Answer: ~a\n\n" mentor-count))))
(let* ((line (parse-file "notes/everybody_codes_e2025_q06_p3.txt"))
(line-length (string-length line)))
(let loop ((i 0) (mentor-count 0))
(if {i < line-length}
(let ((letter (string-ref line i)))
(loop
(1+ i)
(+ mentor-count
(if (char-lower-case? letter)
(let loop ((j (- i 1000)) (mentors-here 0))
(if {j <= (+ i 1000)}
(loop
(1+ j)
(+ mentors-here
(if {(string-ref line {j modulo line-length}) eq? (char-upcase letter)}
(if (and {0 <= j} {j < line-length}) 1000 999)
0)))
mentors-here))
0))))
(format #t "P3 Answer: ~a\n\n" mentor-count))))
Haskell
Hmm. I'm still not very happy with part 3: it's a bit slow and messy. Doing state over the list monad for memoization doesn't work well, so I'm enumerating all possible configurations first and taking advantage of laziness.
import Control.Monad
import Data.Bifunctor
import Data.Ix
import Data.List
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Maybe
import Data.Set.Monad (Set)
import Data.Set.Monad qualified as Set
import Data.Tuple
type Pos = (Int, Int)
readInput :: String -> ((Pos, Pos), Pos, Set Pos, Set Pos)
readInput s =
let grid =
Map.fromList
[ ((i, j), c)
| (i, cs) <- zip [0 ..] $ lines s,
(j, c) <- zip [0 ..] cs
]
in ( ((0, 0), fst $ Map.findMax grid),
fst $ fromJust $ find ((== 'D') . snd) $ Map.assocs grid,
Set.fromList $ Map.keys (Map.filter (== 'S') grid),
Set.fromList $ Map.keys (Map.filter (== '#') grid)
)
moveDragon (i, j) = Set.mapMonotonic (bimap (+ i) (+ j)) offsets
where
offsets = Set.fromList ([id, swap] <*> ((,) <$> [-1, 1] <*> [-2, 2]))
dragonMoves bounds =
iterate (Set.filter (inRange bounds) . (>>= moveDragon)) . Set.singleton
part1 n (bounds, start, sheep, _) =
(!! n)
. map (Set.size . Set.intersection sheep)
. scanl1 Set.union
$ dragonMoves bounds start
part2 n (bounds, dragonStart, sheepStart, hideouts) =
(!! n)
. map ((Set.size sheepStart -) . Set.size)
. scanl'
( \sheep eaten ->
(Set.\\ eaten)
. Set.mapMonotonic (first (+ 1))
. (Set.\\ eaten)
$ sheep
)
sheepStart
. map (Set.\\ hideouts)
$ (tail $ dragonMoves bounds dragonStart)
part3 (bounds, dragonStart, sheepStart, hideouts) =
count (dragonStart, sheepStart)
where
sheepStartByColumn = Map.fromList $ map swap $ Set.elems sheepStart
sheepConfigs =
map
( (Set.fromList . catMaybes)
. zipWith (\j -> fmap (,j)) (Map.keys sheepStartByColumn)
)
. mapM
( ((Nothing :) . map Just)
. (`enumFromTo` (fst $ snd bounds))
)
$ Map.elems sheepStartByColumn
count =
((Map.!) . Map.fromList . map ((,) <*> go))
((,) <$> range bounds <*> sheepConfigs)
go (dragon, sheep)
| null sheep = 1
| otherwise =
(sum . map count) $ do
let movableSheep =
filter (\(_, p) -> p /= dragon || Set.member p hideouts) $
map (\(i, j) -> ((i, j), (i + 1, j))) $
Set.elems sheep
sheepMoves =
if null movableSheep
then [sheep]
else do
(p1, p2) <- movableSheep
return $ Set.insert p2 $ Set.delete p1 sheep
sheep' <- sheepMoves
guard $ all (inRange bounds) sheep'
dragon' <- Set.elems $ moveDragon dragon
guard $ inRange bounds dragon'
let eaten = Set.singleton dragon' Set.\\ hideouts
return (dragon', sheep' Set.\\ eaten)
main = do
readFile "everybody_codes_e2025_q10_p1.txt" >>= print . part1 4 . readInput
readFile "everybody_codes_e2025_q10_p2.txt" >>= print . part2 20 . readInput
readFile "everybody_codes_e2025_q10_p3.txt" >>= print . part3 . readInput
Uiua
Just a dirty great hack and a few minutes of toasty CPU for part3 with live data.
"1,5,2,6,8,4,1,7,3"
βββΈβ @,
&p /+=β(β§β(β΅/-)|Γ·2/β₯) # Part1 --> 4
"1,5,2,6,8,4,1,7,3,5,7,8,2"
βββΈβ @,
Knot β (β(=Β―|=)β©β(Β±-)βΒ°β)
Knots β /β§/β₯β―β_2[β©βKnot]Β°β
β§ββ
&p /+β‘(/+β‘Knots€°ββΒ―)βΒ€+1β2β‘βΈβ§» # Part 2 --> 21
"1,5,2,6,8,4,1,7,3,6"
βββΈβ @,
β(β§
<2+1β‘β§»β΄|β§ββ) # Possible cuts, existing strings.
/β₯+β(β‘ΛββΒ€|β‘(/+β‘KnotsΒ€)βΒ€) # Part3 --> 7
I'm sure there are 17 different graph libraries I could have used for the graph representation and connected components, but it seemed to be in the spirit of the question to write it myself. Nothing interesting about the parent search though -- it's just brute-force comparison.
(ql:quickload :str)
(defun parse-line (line)
(let ((index-and-codes (str:split ":" line)))
(cons (parse-integer (car index-and-codes)) (cadr index-and-codes))))
(defun read-inputs (filename)
(let ((input-lines (uiop:read-file-lines filename)))
(mapcar #'parse-line input-lines)))
(defun can-be-child-of? (parent1 parent2 child)
(loop for i from 0 to (1- (length child))
unless (or (eql (char child i) (char parent1 i))
(eql (char child i) (char parent2 i)))
return nil
finally (return t)))
(defun similarity (genome1 genome2)
(loop for i from 0 to (1- (length genome1))
sum (if (eql (char genome1 i) (char genome2 i)) 1 0)))
(defun main-1 (filename)
(let ((genomes (read-inputs filename)))
(loop for arrangement in '((1 2 3) (2 3 1) (3 1 2))
maximize
(destructuring-bind (parent1-index parent2-index child-index) arrangement
(let ((parent1 (cdr (assoc parent1-index genomes)))
(parent2 (cdr (assoc parent2-index genomes)))
(child (cdr (assoc child-index genomes))))
(if (can-be-child-of? parent1 parent2 child)
(* (similarity parent1 child) (similarity parent2 child))
0))))))
(defun find-parents (genomes child-pair)
(loop named loop1
for tail1 on genomes
for parent1-pair = (car tail1)
do (loop for parent2-pair in (cdr tail1)
when (and
(/= (car parent1-pair) (car child-pair))
(/= (car parent2-pair) (car child-pair))
(can-be-child-of? (cdr parent1-pair) (cdr parent2-pair) (cdr child-pair)))
do (return-from loop1 (cons (car parent1-pair) (car parent2-pair))))
finally (return-from loop1 nil)))
(defun child-relationships (genomes)
(mapcar #'(lambda (child-pair)
(cons (car child-pair) (find-parents genomes child-pair)))
genomes))
(defun main-2 (filename)
(let* ((genomes (read-inputs filename))
(child-relationships (child-relationships genomes)))
(loop for child-rel in child-relationships
sum (destructuring-bind (child-idx . parent-idxs) child-rel
(if (null parent-idxs)
0
(let ((parent1 (cdr (assoc (car parent-idxs) genomes)))
(parent2 (cdr (assoc (cdr parent-idxs) genomes)))
(child (cdr (assoc child-idx genomes))))
(* (similarity parent1 child) (similarity parent2 child))))))))
(defun relationship-graph (child-relationships)
(let ((edges (mapcan #'(lambda (child-rel)
(destructuring-bind (child-idx . parent-idxs) child-rel
(if (null parent-idxs)
nil
(list (cons child-idx (car parent-idxs))
(cons child-idx (cdr parent-idxs))))))
child-relationships))
(graph (make-hash-table)))
(loop for edge in edges
do (destructuring-bind (x . y) edge
(setf (gethash x graph) (cons y (gethash x graph)))
(setf (gethash y graph) (cons x (gethash y graph)))))
graph))
(defun component-of (graph vertex)
(labels ((iter (so-far)
(let ((next (reduce #'union
(mapcar #'(lambda (v) (gethash v graph)) so-far)
:initial-value so-far)))
(if (subsetp next so-far)
next
(iter next)))))
(iter (list vertex))))
(defun all-components (graph vertices)
(labels ((iter (so-far vertices-left)
(if (null vertices-left)
so-far
(let ((comp (component-of graph (car vertices-left))))
(iter (cons comp so-far)
(set-difference vertices-left comp))))))
(iter nil vertices)))
(defun main-3 (filename)
(let* ((genomes (read-inputs filename))
(child-relationships (child-relationships genomes))
(relationship-graph (relationship-graph child-relationships))
(keys (mapcar #'car child-relationships))
(components (all-components relationship-graph keys)))
(reduce #'+
(car (sort components #'(lambda (c1 c2) (> (length c1) (length c2))))))))
Uiua
There's probably a good solution hiding in here, but this ain't it. I originally went for the combinatoric approach for part 3, but it was giving me an answer that turned out to be out by 8, so brute force came to the rescue.
Prep β (
ββ‘βΈβ @\n
Pairs β β‘ββ(Β€|βββΈβ @,)β©Β°β‘Β°β
β(β‘(β‘Pairsββ‘βΈ(Β¬β¦·" > ")Β°β‘)β1|ββ‘βΈβ @,Β°β‘β’)
βΒ―1β§(βΒ°β‘) β[" "]
)
"Oronris,Urakris,Oroneth,Uraketh\nr > a,i,o\ni > p,w\nn > e,r\no > n,m\nk > f,r\na > k\nU > r\ne > t\nO > r\nt > h\n"
Β°β‘β’β½βΈβ‘(/β§ββ(β§ββΒ°β‘))Β€Prep # Part1 --> "Oroneth"
"Xanverax,Khargyth,Nexzeth,Helther,Braerex,Tirgryph,Kharverax\nr > v,e,a,g,y\na > e,v,x,r\ne > r,x,v,t\nh > a,e,v\ng > r,y\ny > p,t\ni > v,r\nK > h\nv > e\nB > r\nt > h\nN > e\np > h\nH > e\nl > t\nz > e\nX > a\nn > v\nx > z\nT > i\n"
/++1ββ‘(/β§ββ(β§ββΒ°β‘))Β€Prep # Part2 --> 23
P β Prep"Khara,Xaryt,Noxer,Kharax\nr > v,e,a,g,y\na > e,v,x,r,g\ne > r,x,v,t\nh > a,e,v\ng > r,y\ny > p,t\ni > v,r\nK > h\nv > e\nB > r\nt > h\nN > e\np > h\nH > e\nl > t\nz > e\nX > a\nn > v\nx > z\nT > i\n"
Keys β ββP
Names β β P
GoodNames β (
Λβ½Namesβ‘(/β§ββ(β§ββΒ°β‘))Β€P # Remove names that fail part2
β½βΈ(Β¬β‘(/+β‘(⨬(0|ββ©βββ§β‘β©β§»β©Β°β‘)>β‘β©(β§»Β°β‘))))βΈΒ€ # Exclude all names that are prefixes of others.
)
# Build up all names and count them. Yuk, slower, but righter.
β0GoodNames
β‘β([β]
β’(
/βββ‘β(β‘β‘ΛββΒ€β‘β£β½β€(=β‘β’)KeysβΈβ£) # Add next chars, collect.
⨬(β|β+ββ§»)βΈ(β₯7β§»β’) # Add to counts if long enough.
| <11β£β³)
β # We don't need the data, just the counts.
)
/+
Common Lisp's loop macro has a pretty crazy list of features. New ones in this solution are for x on xs, which binds the iteration variable x to successive tails (instead of elements) of xs, and maximize, which is an alternative accumulator to sum. There's nothing interesting about the solution itself though -- simple brute-force enumeration.
(ql:quickload :str)
(defun parse-line (line)
(mapcar #'parse-integer (str:split "," line)))
(defun read-inputs (filename)
(let ((input-lines (uiop:read-file-lines filename)))
(parse-line (car input-lines))))
(defun pairs (ns)
(loop for tail on ns
if (not (null (cdr tail)))
collect (cons (car tail) (cadr tail))))
(defun through-center? (nails segment)
(destructuring-bind (x . y) segment
(= (mod (- x y) nails) (/ nails 2))))
(defun main-1 (filename)
(let ((positions (read-inputs filename)))
(loop for segment in (pairs positions)
sum (if (through-center? 32 segment) 1 0))))
(defun crosses? (seg1 seg2)
"When everything is normalized to 1..nails indices, seg1 crosses seg2 iff one of seg2's
endpoints lies strictly between the endpoints of seg1, and the other one of seg2's endpoints
lies strictly below or above the endpoints of seg1."
(destructuring-bind (x1 . y1) seg1
(destructuring-bind (x2 . y2) seg2
(let ((big1 (max x1 y1))
(small1 (min x1 y1)))
(or (and (< small1 x2 big1)
(or (< y2 small1) (> y2 big1)))
(and (or (< x2 small1) (> x2 big1))
(< small1 y2 big1)))))))
(defun main-2 (filename)
(let ((positions (read-inputs filename)))
(loop for seg-list on (pairs positions)
sum (loop for seg2 in (cdr seg-list)
sum (if (crosses? (car seg-list) seg2) 1 0)))))
(defun score (threads seg)
(loop for thread in threads
sum (if (crosses? thread seg) 1 0)))
(defun main-3 (filename)
(let* ((positions (read-inputs filename))
(threads (pairs positions))
(nails 256))
(loop for x1 from 1 to nails
maximize (loop for y1 from (1+ x1) to nails
maximize (score threads (cons x1 y1))))))
Even after 20 years in "nicer", "safer", "more modern" languages ... I still miss Lisp. It's the only language that makes me feel like I'm sculpting in clay, instead of carving stone (Haskell) or laying bricks (Java) or building with Lego (Python). Sure, sometimes the clay comes out kind of lumpy, but that's part of the experience.
(ql:quickload :str)
(ql:quickload :cl-ppcre)
(defun parse-names-line (line)
(str:split "," line))
(defun parse-rule-line (line)
(ppcre:register-groups-bind
(initial finals)
("^([A-Za-z]) > ([A-Za-z,]+)$" line)
(cons (char initial 0)
(mapcar #'(lambda (s) (char s 0)) (str:split "," finals)))))
(defun read-inputs (filename)
(let ((input-lines (uiop:read-file-lines filename)))
(list (cons :names (parse-names-line (car input-lines)))
(cons :rules (mapcar #'parse-rule-line (cddr input-lines))))))
(defun valid? (rules name)
(flet ((valid-pair? (x y)
(member y (cdr (assoc x rules)))))
(loop for i from 0 to (- (length name) 2)
when (not (valid-pair? (char name i) (char name (1+ i))))
return nil
finally (return t))))
(defun main-1 (filename)
(let* ((names-and-rules (read-inputs filename))
(names (cdr (assoc :names names-and-rules)))
(rules (cdr (assoc :rules names-and-rules))))
(loop for name in names
when (valid? rules name)
return name)))
(defun main-2 (filename)
(let* ((names-and-rules (read-inputs filename))
(names (cdr (assoc :names names-and-rules)))
(rules (cdr (assoc :rules names-and-rules))))
(loop for i from 0 to (1- (length names))
sum (if (valid? rules (nth i names)) (1+ i) 0))))
(defun augment (rules prefixes)
(flet ((augment-one (prefix)
(mapcar #'(lambda (c) (str:concat prefix (string c)))
(cdr (assoc (uiop:last-char prefix) rules)))))
(mapcan #'augment-one prefixes)))
(defun main-3 (filename)
(let* ((min-length 7)
(max-length 11)
(names-and-rules (read-inputs filename))
(rules (cdr (assoc :rules names-and-rules)))
(prefixes (remove-if-not #'(lambda (prefix) (valid? rules prefix))
(cdr (assoc :names names-and-rules))))
(names-by-length (make-hash-table)))
(loop for l from (apply #'min (mapcar #'length prefixes)) to max-length
do (setf (gethash l names-by-length)
(remove-duplicates
(append (remove-if-not #'(lambda (prefix) (= l (length prefix))) prefixes)
(augment rules (gethash (1- l) names-by-length)))
:test #'equal)))
(loop for l from min-length to max-length
sum (length (gethash l names-by-length)))))
Uiua
Straightforward until part3. I left the naive approach running while I put together the version below, but of course the better algorithm got there first.
Prep β (
βΛβΒ°ββ‘β‘ # store char with index
β(ββ(β’β’|β‘β‘(Β°β‘β£)))ββΈβ‘ββ’ # group them
ββ―β_6βββΈβ‘(Β°β‘β’β’) # re-arrange
β½=1βΏ2Β°β # drop labels, as we never used them...
)
/+/+β>β©Β°β‘Β°ββ’Prep "ABabACacBCbca" # Part1
/+β‘(/+/+β>β©Β°β‘Β°β)Prep "ABabACacBCbca" # Part2
# Note that the pattern repeats, so the only distinct values
# are in the first run and last run. Everything in between
# will be identical. Only works when distance < string length...
βPrep "AABCBABCABCabcabcABCCBAACBCa"
Rep β 2
Split β [1 -2Rep 1] # multipliers for the three results for each profession.
Dist β 10
β‘(β‘(ββ©β‘)β(β‘+β(€°β‘β’)|Β€ββ‘+β(€°β‘β£)))Β€Γβ‘3β§» # Create the start, middle, end set for each.
/+β‘(/+ΓSplitβ‘(/+/+β(β€Distβ΅-)β©Β°β‘Β°β))
In part 3 we can avoid thinking about the structure of the input string (left vs center vs right portions) and get the efficiency back by building up the counts of available mentors in each window position incrementally. The result is more code and takes a lot of space, but minimizes the number of irregular boundary cases.
(ql:quickload :str)
(defun parse-line (line)
(let ((result (make-hash-table :test #'equal)))
(loop for n from 0 to (1- (length line))
for c = (char line n)
for old = (gethash c result)
do (setf (gethash c result) (cons n old)))
result))
(defun read-inputs (filename)
(parse-line (car (uiop:read-file-lines filename))))
(defun pairs (pos-hash early-type late-type)
(loop for early-index in (gethash early-type pos-hash)
sum (length (remove-if #'(lambda (late-index) (< late-index early-index))
(gethash late-type pos-hash)))))
(defun main-1 (filename)
(pairs (read-inputs filename) #\A #\a))
(defun main-2 (filename)
(let ((pos-hash (read-inputs filename)))
(reduce #'+
(mapcar #'(lambda (type-pair) (apply #'pairs (cons pos-hash type-pair)))
'((#\A #\a) (#\B #\b) (#\C #\c))))))
(defun char-at (base-string pos)
(char base-string (mod pos (length base-string))))
(defun type-window-counts (base-string copies radius type)
(let* ((max-pos (* copies (length base-string)))
(counts (make-array max-pos)))
(setf (aref counts 0)
(loop for i from 0 to radius
sum (if (eql type (char-at base-string i)) 1 0)))
(loop for i from 1 to (1- max-pos)
do (let ((new-count
(+ (aref counts (1- i))
(if (and (< (+ i radius) max-pos)
(eql type (char-at base-string (+ i radius))))
1 0)
(if (and (>= (- i (1+ radius)) 0)
(eql type (char-at base-string (- i (1+ radius)))))
-1 0))))
(setf (aref counts i) new-count)))
counts))
(defun window-counts (base-string copies radius types)
(mapcar #'(lambda (type)
(cons type (type-window-counts base-string copies radius type)))
types))
(defun count-pairs (base-string copies source-type target-type-window-counts)
(let ((max-pos (* copies (length base-string))))
(loop for i from 0 to (1- max-pos)
sum (if (eql source-type (char-at base-string i))
(aref target-type-window-counts i) 0))))
(defun main-3 (filename)
(let* ((base-string (car (uiop:read-file-lines filename)))
(copies 1000)
(radius 1000)
(type-pairs '((#\a #\A) (#\b #\B) (#\c #\C)))
(window-counts (window-counts base-string copies radius (mapcar #'cadr type-pairs))))
(reduce #'+
(mapcar #'(lambda (type-pair)
(count-pairs base-string copies (car type-pair)
(cdr (assoc (cadr type-pair) window-counts))))
type-pairs))))