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

  • Amy@piefed.blahaj.zone
    link
    fedilink
    English
    arrow-up
    4
    ·
    1 month ago

    Haskell

    IntSet was the wrong first choice for part 2 :3

    import 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 input  
    
  • mykl@lemmy.world
    link
    fedilink
    arrow-up
    4
    ·
    1 month ago

    Uiua

    It took me a while to work out the merging of ranges, but I’m very pleased with the solution.

    Run it here

    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  (⊜⋕⊸∊+@010)°□⊜□¬⊸⦷"\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 D
    
  • Zikeji@programming.dev
    link
    fedilink
    English
    arrow-up
    4
    ·
    1 month ago

    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}`);
    
  • eco_game@discuss.tchncs.de
    link
    fedilink
    arrow-up
    3
    ·
    30 days ago

    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

    • chunkystyles@sopuli.xyz
      link
      fedilink
      English
      arrow-up
      1
      ·
      30 days ago

      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
      
  • Strlcpy@1@lemmy.sdf.org
    link
    fedilink
    English
    arrow-up
    2
    ·
    edit-2
    1 month ago

    C

    Repo

    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);
    }
    
  • Deebster@programming.dev
    link
    fedilink
    English
    arrow-up
    2
    ·
    edit-2
    1 month ago

    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(())
    }
    
  • Gobbel2000@programming.dev
    link
    fedilink
    arrow-up
    2
    ·
    1 month ago

    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.

    View on github

    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!();
    
  • Camille@lemmy.ml
    link
    fedilink
    arrow-up
    2
    ·
    30 days ago

    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)
    }
    
  • janAkali@lemmy.sdf.org
    link
    fedilink
    arrow-up
    2
    ·
    edit-2
    1 month ago

    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+1
    

    Full solution at Codeberg: solution.nim

  • Pyro@programming.dev
    link
    fedilink
    arrow-up
    2
    ·
    1 month ago

    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) == 14
    
  • tranzystorekk@beehaw.org
    link
    fedilink
    English
    arrow-up
    2
    ·
    1 month ago

    Janet

    > read part2 description
    > immediately think of naive set solution
    > look at input
    > oh...
    

    I honestly didn’t expect to one-shot part2, I’m usually pretty bad with coming up with complex solutions and coding them up in first-try :P

  • gedhrel@lemmy.world
    link
    fedilink
    arrow-up
    2
    ·
    28 days ago

    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'
    
  • Jayjader@jlai.lu
    link
    fedilink
    English
    arrow-up
    2
    ·
    22 days ago

    (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
      });
    }
    
  • GiantTree@feddit.org
    link
    fedilink
    arrow-up
    2
    ·
    edit-2
    22 days ago

    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 on Github

    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
            }
        }
    }
    
  • Chais@sh.itjust.works
    link
    fedilink
    arrow-up
    2
    ·
    16 days ago

    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))
    
    • CameronDev@programming.devOPM
      link
      fedilink
      arrow-up
      1
      ·
      16 days ago

      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)

      • Chais@sh.itjust.works
        link
        fedilink
        arrow-up
        2
        ·
        edit-2
        14 days ago

        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.