Day 5: Cafeteria
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Haskell
IntSetwas the wrong first choice for part 2 :3import Control.Arrow import Data.Foldable import Data.Ix readInput :: [Char] -> ([(Int, Int)], [Int]) readInput = (map readRange *** (map read . tail)) . break (== "") . lines where readRange = (read *** (read . tail)) . break (== '-') part1 (ranges, ids) = length $ filter (\id -> any (`inRange` id) ranges) ids part2 (ranges, _) = sum $ map rangeSize $ foldl' addRange [] ranges where addRange [] x = [x] addRange (r : rs) x | touching r x = addRange rs $ merge r x | otherwise = r : addRange rs x touching (a, b) (c, d) = not (b < c - 1 || a > d + 1) merge (a, b) (c, d) = (min a c, max b d) main = do input <- readInput <$> readFile "input05" print $ part1 input print $ part2 inputUiua
It took me a while to work out the merging of ranges, but I’m very pleased with the solution.
D ← "3-5\n10-14\n16-20\n12-18\n\n1\n5\n8\n11\n17\n32" # D ← &fras"2025day05.txt" # drop your input file on this pane and uncomment this line to test against your own data. Parse ← ∩(⊜⋕⊸∊+@0⇡10)°□₂⊜□¬⊸⦷"\n\n" Merge ← ⨬( ⨬(⊟ # -> distinct, keep both. | ⊂⊢⟜(↥∩⊣) # -> overlap, merge them. )◡(≤⊓⊣⊢) | ⊙◌ # -> inside, ignore it. )◡(≤∩⊣) Ranges ← ⊙◌⍥⍜⊣(Merge⊙°⊂)◡⋅⧻⊃↙↘1⍆↯∞_2 # Merge pairs of ranges. P₁ ← /+≡⌞(/↥≡⌟(↧⊓⌟≥≤°⊟)) P₂ ← /+≡(+1/-)⊙◌ ⊃P₁ P₂ Ranges Parse DJavascript
Short and sweet. Basically just handle all the range overlaps for part 2 and then do basic subtraction to get the answer.
spoiler
const input = require('fs').readFileSync('input-day5.txt', 'utf-8'); /** @typedef {[number, number]} Range */ /** @type {[Range[], number[]]} */ const [freshRanges, availableIngredients] = input.split("\n\n").map(l => l.split("\n")).map((l, i) => (i === 1) ? l.map(v => parseInt(v, 10)) : l.map(v => v.split('-').map(vv => parseInt(vv, 10)))); let freshOnHand = 0; for (const ingredient of availableIngredients) { for (const [start, stop] of freshRanges) { if (ingredient >= start && ingredient <= stop) { freshOnHand += 1; break; } } } console.log(`Part 1 Answer: ${freshOnHand}`); const sortedRanges = [...freshRanges].sort((a, b) => a[0] - b[0]); const mergedRanges = []; let current = sortedRanges[0]; for (let i = 1; i < sortedRanges.length; i++) { const [nextStart, nextEnd] = sortedRanges[i]; if (nextStart <= current[1] + 1) { current = [current[0], Math.max(current[1], nextEnd)]; } else { mergedRanges.push(current); current = [nextStart, nextEnd]; } } mergedRanges.push(current); const freshIngredientCount = mergedRanges.reduce((acc, [start, stop]) => acc + ((stop + 1) - start), 0) console.log(`Part 2 Answer: ${freshIngredientCount}`);Kotlin
I first solved the puzzle with my own horrible range-merging algorithm, had quite a few nasty off by one errors. I quickly replaced it with the “normal” merge-algorithm after getting the right solution.
If you really want to see my abomination, it’s still in the commits on my repo.
Solution
class Day05 : Puzzle { val validIds = mutableSetOf<LongRange>() val ids = mutableListOf<Long>() override fun readFile() { val input = readInputFromFile("src/main/resources/a2025/day05.txt") val inputList = input.split("\n\n", limit = 2) validIds.addAll( inputList[0].lines().filter { it.isNotBlank() } .map { it.split("-").map { it.toLong() } } .map { LongRange(it[0], it[1]) } ) ids.addAll(inputList[1].lines().filter { it.isNotBlank() }.map { it.toLong() }) } override fun solvePartOne(): String { return ids.count { id -> validIds.any { id in it } }.toString() } override fun solvePartTwo(): String { val idsSorted = validIds.sortedBy { it.first } val merged = mutableSetOf<LongRange>() var current = idsSorted.first() for (i in 1..<idsSorted.size) { val next = idsSorted[i] if (next.first <= current.last) { current = LongRange(current.first, max(current.last, next.last())) } else { merged.add(current) current = next } } merged.add(current) return merged.sumOf { it.last + 1 - it.first }.toString() } }full code on Codeberg
It’s amusing just how similar our solutions are today. The only real issue I had today was not considering both sides of each range and only taking the last part of the range in the sort order.
var ingredientRanges: MutableList<LongRange> = mutableListOf() var ingredients: MutableList<Long> = mutableListOf() fun main() { val input = getInput(5) parseInput1(input) var count = 0L ingredientRanges.sortBy { it.first } var i = 0 while (i < ingredientRanges.size) { var j = i + 1 while (j < ingredientRanges.size && doRangesOverlap(ingredientRanges[i], ingredientRanges[j])) { ingredientRanges[i] = LongRange(ingredientRanges[i].first, max(ingredientRanges[i].last, ingredientRanges[j].last)) j++ } count += ingredientRanges[i].last - ingredientRanges[i].first + 1 i = j } println(count) } fun parseInput1(input: String) { val split = input.split("\n\n") split[0].lines() .filter { it.isNotBlank() } .forEach { val range = it.split("-") ingredientRanges.add(LongRange(range[0].toLong(), range[1].toLong())) } split[1].lines() .filter { it.isNotBlank() } .forEach { ingredients.add(it.toLong()) } } fun doRangesOverlap(left: LongRange, right: LongRange): Boolean = right.first in left || right.first - 1 == left.last
C
Sweet one. Glad the merge could be done with one n2/2 scan and no sorting or moving, which will make the assembly port a bit easier. Speaking of which, I’m still at day 3 there, fighting not the puzzles but the 64K .COM file limit!
Code
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <assert.h> #define MR 256 #define MIN(a,b) ((a)<(b)?(a):(b)) #define MAX(a,b) ((a)>(b)?(a):(b)) struct range { uint64_t lo, hi; }; static struct range rs[MR]; static int nrs; int main() { uint64_t p1=0,p2=0, val; int i,j; char buf[64]; for (; fgets(buf, sizeof(buf), stdin); nrs++) { assert(nrs < MR); if (sscanf(buf, "%lu-%lu", &rs[nrs].lo, &rs[nrs].hi) != 2) break; } for (i=0; i<nrs-1; i++) for (j=i+1; j<nrs; j++) if (rs[i].lo <= rs[j].hi && rs[i].hi >= rs[j].lo) { rs[j].lo = MIN(rs[i].lo, rs[j].lo); rs[j].hi = MAX(rs[i].hi, rs[j].hi); rs[i].lo = rs[i].hi = 0; } while (scanf("%lu", &val) == 1) for (i=0; i<nrs; i++) if (val >= rs[i].lo && val <= rs[i].hi) { p1++; break; } for (i=0; i<nrs; i++) if (rs[i].lo) p2 += rs[i].hi - rs[i].lo + 1; printf("05: %lu %lu\n", p1, p2); }Rust
I didn’t look at the input at first so tried a naive version with sets, but it was too slow even for part one. I got smarter and wrote a version with less code that runs in under half a millisecond.
type Id = usize; struct Ingredients { fresh: Vec<RangeInclusive<Id>>, available: HashSet<Id>, } impl Ingredients { fn is_fresh(&self, id: Id) -> bool { self.fresh.iter().any(|range| range.contains(&id)) } fn num_fresh_available(&self) -> usize { self.available .iter() .filter(|&&id| self.is_fresh(id)) .count() } fn num_fresh_all(&self) -> usize { self.fresh .iter() .map(|range| 1 + range.end() - range.start()) .sum() } } fn merge_ranges(mut ranges: Vec<RangeInclusive<Id>>) -> Vec<RangeInclusive<Id>> { if ranges.is_empty() { return ranges; } ranges.sort_by_key(|range| *range.start()); let mut merged = Vec::new(); let mut start = ranges[0].start(); let mut end = ranges[0].end(); for range in ranges.iter().skip(1) { if range.start() > end { merged.push(RangeInclusive::new(*start, *end)); start = range.start(); end = range.end(); } if range.end() > end { end = range.end(); } } merged.push(RangeInclusive::new(*start, *end)); merged } impl FromStr for Ingredients { type Err = Report; fn from_str(s: &str) -> Result<Self, Self::Err> { let Some((fresh_str, avail_str)) = s.split_once("\n\n") else { bail!("missing divider"); }; let fresh = fresh_str .lines() .map(|l| -> Result<RangeInclusive<_>, Self::Err> { let (start, end) = l.split_once('-').ok_or_eyre("bad range")?; let start: Id = start.parse()?; let end: Id = end.parse()?; Ok(start..=end) }) .collect::<Result<_, _>>()?; let available = avail_str .lines() .map(|l| l.parse()) .collect::<Result<_, _>>()?; Ok(Ingredients { fresh: merge_ranges(fresh), available, }) } }Full code
use std::{collections::HashSet, fs, ops::RangeInclusive, str::FromStr}; use color_eyre::eyre::{OptionExt, Report, Result, bail}; #[derive(Debug, PartialEq, Eq)] struct Ingredients { fresh: Vec<RangeInclusive<Id>>, available: HashSet<Id>, } type Id = usize; impl Ingredients { fn is_fresh(&self, id: Id) -> bool { self.fresh.iter().any(|range| range.contains(&id)) } fn num_fresh_available(&self) -> usize { self.available .iter() .filter(|&&id| self.is_fresh(id)) .count() } fn num_fresh_all(&self) -> usize { self.fresh .iter() .map(|range| 1 + range.end() - range.start()) .sum() } } fn merge_ranges(mut ranges: Vec<RangeInclusive<Id>>) -> Vec<RangeInclusive<Id>> { if ranges.is_empty() { return ranges; } ranges.sort_by_key(|range| *range.start()); let mut merged = Vec::new(); let mut start = ranges[0].start(); let mut end = ranges[0].end(); for range in ranges.iter().skip(1) { if range.start() > end { merged.push(RangeInclusive::new(*start, *end)); start = range.start(); end = range.end(); } if range.end() > end { end = range.end(); } } merged.push(RangeInclusive::new(*start, *end)); merged } impl FromStr for Ingredients { type Err = Report; fn from_str(s: &str) -> Result<Self, Self::Err> { let Some((fresh_str, avail_str)) = s.split_once("\n\n") else { bail!("missing divider"); }; let fresh = fresh_str .lines() .map(|l| -> Result<RangeInclusive<_>, Self::Err> { let (start, end) = l.split_once('-').ok_or_eyre("bad range")?; let start: Id = start.parse()?; let end: Id = end.parse()?; Ok(start..=end) }) .collect::<Result<_, _>>()?; let available = avail_str .lines() .map(|l| l.parse()) .collect::<Result<_, _>>()?; Ok(Ingredients { fresh: merge_ranges(fresh), available, }) } } fn part1(filepath: &str) -> Result<usize> { let input = fs::read_to_string(filepath)?; let ingrd = Ingredients::from_str(&input).unwrap(); Ok(ingrd.num_fresh_available()) } fn part2(filepath: &str) -> Result<usize> { let input = fs::read_to_string(filepath)?; let ingrd = Ingredients::from_str(&input).unwrap(); Ok(ingrd.num_fresh_all()) } fn main() -> Result<()> { color_eyre::install()?; println!("Part 1: {}", part1("d05/input.txt")?); println!("Part 2: {}", part2("d05/input.txt")?); Ok(()) }Rust
Tried to outsmart part 2 by not merging ranges but just subtracting overlapping ranges from the current range’s size, but then overlapping overlaps are subtracted more than once. So I ended up merging the ranges. They are kept in a list that is sorted by their starting position. Also, transforming the inclusive ranges in the input into exclusive ranges made things quite a bit easier.
use std::ops::Range; fn parse_input(input: &str) -> (Vec<Range<u64>>, Vec<u64>) { let ranges: Vec<_> = input .lines() .take_while(|l| !l.is_empty()) .map(|l| { let (a, b) = l.split_once('-').unwrap(); a.parse().unwrap()..b.parse::<u64>().unwrap() + 1 }) .collect(); let nums = input .lines() .skip(ranges.len() + 1) .map(|n| n.parse().unwrap()) .collect(); (ranges, nums) } fn part1(input: String) { let (ranges, nums) = parse_input(&input); let count = nums .iter() .filter(|n| ranges.iter().any(|r| r.contains(n))) .count(); println!("{count}"); } fn part2(input: String) { let (ranges, _) = parse_input(&input); // Ranges are added to this Vec always sorted by start and non-overlapping let mut merged: Vec<Range<u64>> = Vec::with_capacity(ranges.len()); for r in ranges { // Find index of first intersecting range let first_int_o = merged.iter().position(|m| { // Intersection range (if any) let int_start = r.start.max(m.start); let int_end = r.end.min(m.end); int_start < int_end }); if let Some(first_int) = first_int_o { // Exclusive let last_int = merged.len() - merged .iter() .rev() .position(|m| { let int_start = r.start.max(m.start); let int_end = r.end.min(m.end); int_start < int_end }) .unwrap(); // New range replacing all intersecting ranges let new = r.start.min(merged[first_int].start)..r.end.max(merged[last_int - 1].end); merged[first_int] = new; for i in (first_int + 1)..last_int { merged.remove(i); } } else { // Does not overlap with anything. Find index that keeps sorting let idx = merged .iter() .position(|m| m.start > r.start) .unwrap_or(merged.len()); merged.insert(idx, r); } } let count = merged.iter().map(|r| r.end - r.start).sum::<u64>(); println!("{count}"); } util::aoc_main!();Go
I started by not sorting the ranges in the input stream and try to merge everything together. It didn’t work, but the same algorithm with sorted input does. I guess I’m missing something important, but I tested my code and can’t find the corner case that make it fail. Bah…
day05.go
package main import ( "aoc/utils" "cmp" "fmt" "slices" "strconv" "strings" ) type idrange struct { start, end int } func (rng idrange) contains(entry int) bool { return rng.start <= entry && entry <= rng.end } func (rng idrange) length() int { return (rng.end - rng.start) + 1 } type rangeSet []idrange func (r *rangeSet) addRange(rng idrange) { containsStartIdx := slices.IndexFunc(*r, func(elem idrange) bool { return elem.contains(rng.start) }) containsEndIdx := slices.IndexFunc(*r, func(elem idrange) bool { return elem.contains(rng.end) }) // The range overlaps to other ranges. if containsStartIdx != -1 && containsEndIdx != -1 { // If it is in fact contained inside one range, it is ignored. if containsStartIdx == containsEndIdx { return } before := (*r)[containsStartIdx] after := (*r)[containsEndIdx] before.end = after.end (*r)[containsStartIdx] = before *r = slices.Delete(*r, containsStartIdx+1, containsEndIdx+1) } else if containsEndIdx != -1 { // If the range's end overlaps with another range, that range is // extended on the front to start like the range in argument. after := (*r)[containsEndIdx] after.start = rng.start (*r)[containsEndIdx] = after smallestAfterIdx := slices.IndexFunc(*r, func(elem idrange) bool { return rng.start < elem.start }) if smallestAfterIdx != -1 && smallestAfterIdx != containsEndIdx+1 { *r = slices.Delete(*r, smallestAfterIdx, containsEndIdx) } } else if containsStartIdx != -1 { // If the range's start overlaps with another range, that range is // extended on the back to start like the range in argument. before := (*r)[containsStartIdx] before.end = rng.end (*r)[containsStartIdx] = before smallestAfterIdx := slices.IndexFunc(*r, func(elem idrange) bool { return rng.end < elem.end }) if smallestAfterIdx != -1 && smallestAfterIdx != containsStartIdx+1 { *r = slices.Delete(*r, containsStartIdx+1, smallestAfterIdx) } } else { // If the range is standalone, it is added at the right position. afterIdx := slices.IndexFunc(*r, func(elem idrange) bool { return rng.start < elem.start }) if afterIdx == -1 { *r = append(*r, rng) } else { *r = slices.Insert(*r, afterIdx, rng) } } } func (r rangeSet) contains(entry int) bool { for _, rng := range r { if rng.contains(entry) { return true } } return false } func (r rangeSet) length() int { sum := 0 for _, rng := range r { sum += rng.length() } return sum } func parseRange(line string) (idrange, error) { bounds := strings.Split(line, "-") start, err1 := strconv.Atoi(bounds[0]) end, err2 := strconv.Atoi(bounds[1]) if err1 != nil { return idrange{}, err1 } if err2 != nil { return idrange{}, err2 } return idrange{ start: start, end: end, }, nil } func parseRangeSet(input chan string) (rangeSet, error) { rangeSet := rangeSet{} ranges := []idrange{} for line := range input { if line == "" { break } rng, err := parseRange(line) if err != nil { return nil, err } ranges = append(ranges, rng) } slices.SortFunc(ranges, func(a, b idrange) int { return cmp.Compare(a.start, b.start) }) for _, rng := range ranges { rangeSet.addRange(rng) } return rangeSet, nil } func getNumberChannel(input chan string) chan int { ch := make(chan int, cap(input)) go func() { for line := range input { num, _ := strconv.Atoi(line) ch <- num } close(ch) }() return ch } func stepOne(input chan string) (int, error) { rngSet, errParse := parseRangeSet(input) if errParse != nil { return 0, errParse } numCh := getNumberChannel(input) count := 0 for entry := range numCh { if rngSet.contains(entry) { count++ } } return count, nil } func stepTwo(input chan string) (int, error) { rngSet, err := parseRangeSet(input) if err != nil { return 0, err } return rngSet.length(), nil } func main() { inputFile := utils.FilePath("day05.txt") utils.RunStep(utils.ONE, inputFile, stepOne) utils.RunStep(utils.TWO, inputFile, stepTwo) }Nim
Huh, I didn’t expect two easy days in a row.
Part 1 is a range check. Part 2 is a range merge.Runtime: ~720 µs
type AOCSolution[T,U] = tuple[part1: T, part2: U] proc merge[T](ranges: var seq[Slice[T]]) = ranges.sort(cmp = proc(r1, r2: Slice[T]): int = cmp(r1.a, r2.a)) var merged = @[ranges[0]] for range in ranges.toOpenArray(1, ranges.high): if range.a <= merged[^1].b: if range.b > merged[^1].b: merged[^1].b = range.b else: merged.add range ranges = merged proc solve(input: string): AOCSolution[int, int] = let chunks = input.split("\n\n") var freshRanges = chunks[0].splitLines().mapIt: let t = it.split('-'); t[0].parseInt .. t[1].parseInt freshRanges.merge() block p1: let availableFood = chunks[1].splitLines().mapIt(parseInt it) for food in availableFood: for range in freshRanges: if food in range: inc result.part1 break block p2: for range in freshRanges: result.part2 += range.b-range.a+1Full solution at Codeberg: solution.nim
Python
Not too bad though part2 could become difficult if you don’t realize the simple method to merge overlapping intervals.
see code
# takes a list of (start, end) ranges and merges overlapping ones def merge_overlapping_ranges(ranges: list[tuple[int, int]]) -> list[tuple[int, int]]: # sort ranges by start so that overlaps are adjacent ranges.sort(key=lambda r: r[0]) merged_ranges = [] # keep track of a current range that could be merged into curr_start, curr_end = ranges[0] for start, end in ranges: if curr_start <= start <= curr_end: # overlap, extend current range curr_end = max(curr_end, end) else: # no overlap, save current range and record the new one merged_ranges.append((curr_start, curr_end)) curr_start, curr_end = start, end # finally, don't forget to add the last range that's being tracked merged_ranges.append((curr_start, curr_end)) return merged_ranges def part1(data: str): # split input freshness_data, ingredients_data = data.split("\n\n") # parse data freshness_ranges = [tuple(map(int, f.split('-'))) for f in freshness_data.splitlines()] freshness_ranges = merge_overlapping_ranges(freshness_ranges) # optimization: merge overlapping ranges ingredients = [int(i) for i in ingredients_data.splitlines()] # checks if an ingredient is fresh # this could've been done with binary search, but linear search is simpler and fast enough def is_fresh(ingredient: int) -> bool: for start, end in freshness_ranges: if start <= ingredient <= end: # found return True if start > ingredient: # quit early since we are past any range that could've contained the ingredient return False return False # count all fresh ingredients return sum(is_fresh(i) for i in ingredients) def part2(data: str): # split input freshness_data, _ = data.split("\n\n") # parse data freshness_ranges = [tuple(map(int, f.split('-'))) for f in freshness_data.splitlines()] freshness_ranges = merge_overlapping_ranges(freshness_ranges) # merge overlapping ranges # return total count of fresh ingredients # we add +1 because both start and end are inclusive return sum(end - start + 1 for start, end in freshness_ranges) sample = """3-5 10-14 16-20 12-18 1 5 8 11 17 32""" assert part1(sample) == 3 assert part2(sample) == 14There’s an alternative approach to merging ranges. Put starting and ending points into a heap then scan it, keeping track of the number of open ranges present. Something like this:
mergeRanges rs = let -- We put in this order so we count openings before closings starts = map (\(a, b) -> (a, -1)) rs ends = map (\(a, b) -> (b, 1)) rs heap = H.fromList (starts ++ ends) :: H.MinHeap (Int, Int) in mergeNo heap where -- not in a range currently mergeNo :: H.MinHeap (Int, Int) -> [R] mergeNo h = case H.view h of Nothing -> [] Just ((a, -1), h') -> mergeYes a 1 h' -- in a range mergeYes :: Int -> Int -> H.MinHeap (Int, Int) -> [R] mergeYes start height h = let Just ((v, delta), h') = H.view h newHeight = height - delta in if newHeight == 0 then (start, v) : mergeNo h' else mergeYes start newHeight h'(Browser-based) Javascript
The good ol’ range merge is back! I’ve done the past few years in Rust, which has first-class “support” for inclusive ranges, so I was dreading having to re-implement things. Turns out when you can ignore lifetimes things get simpler.
Code
function part1(inputText) { const [freshRangeDefs, availableIds] = inputText.trim().split('\n\n'); const parsedIds = new Set(availableIds.trim().split('\n').map(strId => Number.parseInt(strId, 10))); const parsedRanges = freshRangeDefs.trim().split('\n').map(rangeDef => rangeDef.split('-').map(strBound => Number.parseInt(strBound, 10))); for (const id of parsedIds) { let fresh = false; for (const [lowerBound, upperBound] of parsedRanges) { if (lowerBound <= id && id <= upperBound) { fresh = true; break; } } if (!fresh) { parsedIds.delete(id) } } return parsedIds.size; } { const start = performance.now(); const result = part1(document.body.textContent); const end = performance.now(); console.info({ day: 5, part: 1, time: end - start, result }); } function part2(inputText) { const [freshRangeDefs] = inputText.trim().split('\n\n'); const parsedRanges = freshRangeDefs.trim().split('\n').map(rangeDef => rangeDef.split('-').map(strBound => Number.parseInt(strBound, 10))); parsedRanges.sort(([lowerA, upperA], [lowerB, upperB]) => lowerA - lowerB); const merged = [parsedRanges[0]]; for (const [parsedLower, parsedUpper] of parsedRanges) { let inserted = false; for (let mIndex = 0; mIndex < merged.length; mIndex++) { const [mLower, mUpper] = merged[mIndex]; if (mUpper < parsedLower) { // parsed is completely "above" merged continue } else if (parsedUpper < mLower) { // parsed is completely "below" merged merged.splice(mIndex, 0, [parsedLower, parsedUpper]); inserted = true; break; } else { // parsed and merged intersect somehow/somewhere) merged[mIndex][0] = Math.min(parsedLower, mLower) merged[mIndex][1] = Math.max(parsedUpper, mUpper) inserted = true; break; } } if (!inserted) { // parsed is completely "above" ALL already-merged ranges merged.push([parsedLower, parsedUpper]); } } return merged.reduce((accu, [lower, upper]) => accu + upper - lower + 1, 0) } { const exampleText = `3-5 10-14 16-20 12-18 1 5 8 11 17 32 ` const start = performance.now(); // const result = part2(exampleText); const result = part2(document.body.textContent); const end = performance.now(); console.info({ day: 5, part: 2, time: end - start, result }); }Kotlin
I like this one. The first part could be solved trivially with a naive approach. The second part is also not too complicated.
I started out with a simple merging algorithm that would check all ranges, except the one to merge, for overlaps.
With clever sorting, I can skip the search for mergable ranges and just iterate in reverse and build larger and larger ranges.
I haven’t tried to merge adjacent ranges like 1…2 and 3…4 to 1…4. The solution is fast enough already when JIT/C2 compiled (200-250 µs).Code
class Day05 : AOCSolution { override val year = 2025 override val day = 5 override fun part1(inputFile: String): String { val (freshIngredients, availableIngredients) = readResourceTwoParts(inputFile) val freshRanges = buildRanges(freshIngredients) val ingredientIds = availableIngredients.lines().filterNot(String::isEmpty).map(String::toLong) val count = ingredientIds.count { id -> freshRanges.any { range -> id in range } } return count.toString() } override fun part2(inputFile: String): String { val (freshIngredients, _) = readResourceTwoParts(inputFile) val freshRanges = buildRanges(freshIngredients) return freshRanges.sumOf { range -> range.last - range.first + 1 }.toString() } private companion object { private fun buildRanges(ingredients: String): List<LongRange> { val lines = ingredients.lines() val ranges = MutableList(lines.size) { i -> val line = lines[i] val hyphen = line.indexOf('-') val lower = line.take(hyphen).toLong() val upper = line.substring(hyphen + 1).toLong() LongRange(lower, upper) } // Sort the ranges // The ones with the smallest ID and the least upper end // get sorted to the beginning. // This allows for easy merging, as overlapping ranges are always adjacent ranges.sortWith(Comparator { r1, r2 -> val first = r1.first.compareTo(r2.first) if (first != 0) { first } else { r1.last.compareTo(r2.last) } }) // Merge adjacent ranges backwards, modifying the list in-place for (i in ranges.lastIndex downTo 1) { val lowerRange = ranges[i - 1] val upperRange = ranges[i] // The two ranges do overlap, because the tail of the first range // is in the second range. if (upperRange.first <= lowerRange.last) { ranges[i - 1] = LongRange( lowerRange.first, maxOf(lowerRange.last, upperRange.last) ) ranges.removeAt(i) } } return ranges } } }Again part 2 took me way longer than I would’ve liked and also than feels appropriate for the simplicity of the solution I finally came up with.
Turned out quite fast, thanks to the ranges.Python
from pathlib import Path from typing import List from itertools import combinations def parse_input(input: str) -> tuple[set[range], list[int]]: parts = input.split("\n\n") fresh = set((lambda r: range(int(r[0]), int(r[1]) + 1))(line.split("-")) for line in parts[0].splitlines()) return (fresh, list(map(int, parts[1].splitlines()))) def merge_ranges(a: range, b: range) -> List[range]: if a.stop <= b.start or b.stop <= a.start: return [a, b] return [range(min(a.start, b.start), max(a.stop, b.stop))] def part_one(input: str) -> int: fresh, available = parse_input(input) return len(list(filter(None, [any(i in r for r in fresh) for i in available]))) def part_two(input: str) -> int: fresh, _ = parse_input(input) while True: for a, b in combinations(fresh, 2): if len(m := merge_ranges(a, b)) == 1: fresh.remove(a) fresh.remove(b) fresh.add(m[0]) break else: break return sum(map(len, fresh)) if __name__ == "__main__": input = Path("_2025/_5/input").read_text("utf-8") print(part_one(input)) print(part_two(input))That is nice and simple, power of python I guess. How quick was the pt2 solve? I could imagine that being pathalogically slow with the wrong ordering of inputs?
Eg: (99,100),(0,1),…, (95,96), (96,97), (97,98), (98,99)
I haven’t timed it, but easily below a second.
Could that be optimised? Most certainly.Due to the ranges being in a set, rather than a list, the input order doesn’t matter anyway. And the set really does a lot of heavy lifting for making the code so concise. You’ll need a bunch of boilerplate for list maintenance, especially if you continuously keep it sorted.
The set also removed 8 duplicates I had in the input.










