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
There’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'Uiua
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 DWell, I guess I’ve decided to do AoC this year. Trivial problem, though I shamelessly stole some range-merging code from AoC 2022 (it was when I was still well in the learning phase of Rust). If you can’t look at old code and wonder WTF you were thinking, have you really gotten any better?
Solution (mostly uninteresting)
pub fn day05(input: &str) -> (u64, u64) { let mut part1 = 0; let mut part2 = 0; let mut ranges = Vec::new(); let mut lines = input.trim().lines(); for line in lines.by_ref() { if line.is_empty() { break; } let range = line .split_once('-') .map(|(l, h)| [l.parse::<u64>().unwrap(), h.parse::<u64>().unwrap()]) .unwrap(); ranges.push(range); } let ranges = combine_ranges(ranges); for idx in lines { for r in ranges.iter() { if (r[0]..=r[1]).contains(&idx.parse::<u64>().unwrap()) { part1 += 1; break; } } } for r in ranges { part2 += r[1] - r[0] + 1; } (part1, part2) }
combine_rangesWARNING : Hideous, triggers Clippy, not blazingly fastfn combine_ranges(ranges: Vec<[u64; 2]>) -> Vec<[u64; 2]> { let mut temp_ranges = ranges; let mut comp_ranges: Vec<[u64; 2]> = Vec::new(); let mut run_again: bool = true; while run_again { run_again = false; comp_ranges = Vec::new(); 'outer: for i in 0..temp_ranges.len() { for j in 0..comp_ranges.len() { if temp_ranges[i][0] <= comp_ranges[j][0] && comp_ranges[j][1] <= temp_ranges[i][1] { //temp covers all of comp comp_ranges[j][0] = temp_ranges[i][0]; comp_ranges[j][1] = temp_ranges[i][1]; run_again = true; continue 'outer; } else if comp_ranges[j][0] <= temp_ranges[i][0] && temp_ranges[i][1] <= comp_ranges[j][1] { //comp covers all of temp run_again = true; continue 'outer; } else if temp_ranges[i][0] <= comp_ranges[j][0] && comp_ranges[j][0] <= temp_ranges[i][1] && temp_ranges[i][1] <= comp_ranges[j][1] { //grow left comp_ranges[j][0] = temp_ranges[i][0]; run_again = true; continue 'outer; } else if comp_ranges[j][0] <= temp_ranges[i][0] && temp_ranges[i][0] <= comp_ranges[j][1] && comp_ranges[j][1] <= temp_ranges[i][1] { //grow right comp_ranges[j][1] = temp_ranges[i][1]; run_again = true; continue 'outer; } } comp_ranges.push(temp_ranges[i]); } temp_ranges = comp_ranges.clone(); } comp_ranges }Nothing interesting here. I was tripped up momentarily by the fact that Common Lisp
sort, while it’s a destructive operation, does not leave its input argument equal to its result! Typically you see either “nondestructive, returns a value” (Pythonsorted()) or “destructive, leaves the object in the final state” (Pythonlist.sort()). Old school Lispsortreturns the sorted list and the input structure is not guaranteed to be meaningful afterward. Hope you weren’t using that pair for anything; it now points to hell.(ql:quickload :str) (defun read-inputs (filename) (let* ((input-lines (uiop:read-file-lines filename)) (range-lines (remove-if-not #'(lambda (s) (find #\- s)) input-lines)) (id-lines (remove-if #'(lambda (s) (or (zerop (length s)) (find #\- s))) input-lines))) (flet ((parse-range (line) (mapcar #'parse-integer (str:split "-" line)))) (cons (mapcar #'parse-range range-lines) (mapcar #'parse-integer id-lines))))) (defun fresh? (fresh-ranges id) "Return the first fresh range containing id, or nil if there is no such range. Assumes the fresh ranges are sorted to enable early exit." (loop for fresh-range in fresh-ranges when (<= (car fresh-range) id (cadr fresh-range)) return fresh-range when (> (car fresh-range) id) return nil finally (return nil))) (defun range< (range1 range2) (destructuring-bind (a1 b1) range1 (destructuring-bind (a2 b2) range2 (or (< a1 a2) (and (= a1 a2) (< b1 b2)))))) (defun main-1 (filename) (destructuring-bind (fresh-ranges . ids) (read-inputs filename) (let ((sorted-fresh-ranges (sort fresh-ranges #'range<))) (loop for id in ids sum (if (fresh? sorted-fresh-ranges id) 1 0))))) (defun consolidate (fresh-ranges) "Remove redundancy in a sorted list of fresh ranges: emit non-overlapping ranges." (let ((result nil) (current-range (car fresh-ranges))) (loop for fresh-range in (cdr fresh-ranges) do (if (<= (car fresh-range) (cadr current-range)) (setf current-range (list (car current-range) (max (cadr current-range) (cadr fresh-range)))) (progn (setf result (cons current-range result)) (setf current-range fresh-range))) finally (setf result (cons current-range result))) result)) (defun main-2 (filename) (destructuring-bind (fresh-ranges . ids) (read-inputs filename) (let ((sorted-fresh-ranges (sort fresh-ranges #'range<))) (reduce #'+ (mapcar #'(lambda (range) (1+ (- (cadr range) (car range)))) (consolidate sorted-fresh-ranges))))))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
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) }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!();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(()) }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); }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 inputRust
#[test] fn test_y2025_day5_part2() { let input = std::fs::read_to_string("input/2025/day_5.txt").unwrap(); let (fresh, _) = input.split_once("\n\n").unwrap(); let mut fresh = fresh .lines() .map(|l| { let (p1, p2) = l.split_once("-").unwrap(); (p1.parse::<usize>().unwrap(), p2.parse::<usize>().unwrap()) }) .collect::<Vec<(usize, usize)>>(); fresh.sort_by_key(|a| a.0); let mut non_overlapping = vec![*fresh.first().unwrap()]; for range in fresh[1..].iter() { let last = *non_overlapping.last().unwrap(); if range.0 > last.1 { // println!("Non overlapping: {range:?} -> {last:?}"); non_overlapping.push((range.0, range.1)); continue; } if range.0 <= last.1 && range.1 > last.1 { // println!("Overlapping: {range:?} -> {last:?}"); let new_last = (last.0, range.1); non_overlapping.pop(); non_overlapping.push(new_last); continue; } // println!("{range:?} is entirely within {last:?}"); } let mut count = 0; for r in &non_overlapping { count += r.1 - r.0 + 1; } assert_eq!(count, 352556672963116); println!("{}", count); }Took me way longer than it should have to work out pt2, got tripped up by a
<instead of<=.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) == 14Nim
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
Javascript
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}`);









