this post was submitted on 05 Dec 2023
34 points (100.0% liked)

Advent Of Code

774 readers
15 users here now

An unofficial home for the advent of code community on programming.dev!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

AoC 2023

Solution Threads

M T W T F S S
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 1 year ago
MODERATORS
34
submitted 11 months ago* (last edited 11 months ago) by Ategon to c/advent_of_code
 

Day 5: If You Give a Seed a Fertilizer


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)
  • Code block support is not fully rolled out yet but likely will be in the middle of the event. Try to share solutions as both code blocks and using something such as https://topaz.github.io/paste/ , pastebin, or github (code blocks to future proof it for when 0.19 comes out and since code blocks currently function in some apps and some instances as well if they are running a 0.19 beta)

FAQ


🔒This post will be unlocked when there is a decent amount of submissions on the leaderboard to avoid cheating for top spots

🔓 Unlocked after 27 mins (current record for time, hard one today)

all 41 comments
sorted by: hot top controversial new old
[–] [email protected] 7 points 11 months ago* (last edited 11 months ago) (2 children)

Rust

Ooof. Part 1 was easy enough, but for part two I initially went with the naive solution of trying every single seed which took more than a minute (I never really measured). Although that got me the right answer, to me that was just unacceptable.

I proceeded to try and combine all mappings into one but gave up after spending way too much time on it.

Then I had the idea that the lowest number in the end must lie at the beginning of a range somewhere. Either the start of a seed range in the beginning or the start of a range in one of the mappings. Any in-between numbers must end up with a higher result. So I considered the start points of all ranges, went through the mappings in reverse to find out if that point is actually within a seed range, and only tested those starting points.

Finally I had only 183 points to test which ran much faster (0.9ms).

[–] [email protected] 3 points 11 months ago

Then I had the idea that the lowest number in the end must lie at the beginning of a range somewhere. Either the start of a seed range in the beginning or the start of a range in one of the mappings.

This really helped me out. I was stuck on either trying every single seed, or working backwards and trying every single location from 0 to infinity, and couldn't wrap my head around how to filter down the list to be manageable. Your comment made it all make sense.

In the end, was able to work backwards with the 172 lowest locations in each range to get potential seeds, and from there was able to get a short list of 89 valid seeds (including the original seed values) to then figure out which one has the shortest location.

Thanks!

[–] [email protected] 1 points 11 months ago* (last edited 11 months ago) (1 children)

I'm a little confused about this one. The mappings are total, that is any number that is not defined explicitly gets mapped to itself. So it's easy to create an example where the lowest number does not get mentioned within a range:

seeds: 0 3

seed-to-soil map:
10 0 2

soil-to-fertilizer map:
100 200 5

fertilizer-to-water map:
100 200 5

water-to-light map:
100 200 5

light-to-temperature map:
100 200 5

temperature-to-humidity map:
100 200 5

humidity-to-location map:
100 200 5

Here, we have seeds 0, 1 and 2. seed 0 gets mapped to location 10, seed 1 gets mapped to location 11 and seed 2 gets mapped to location 2. That means location 2 would be the answer, but it's not a start of any range. I guess this just doesn't happen in any of the inputs?

EDIT: actually it's double weird. If you implemented a backwards search, that is you create reverse mappings and then try out all locations (which is what I and many others did), the result of the above example is location 0, whereas if you create a forwards brute force of all seeds, the result is 2. For the reverse approach to work in all cases, the mappings would have to be bijective.

[–] [email protected] 1 points 11 months ago

Indeed, my solution fails on this input (returns 10, which is the location to seed 0), but it can be easily solved by also adding the ends of each range as well.

Maybe the input was quite forgiving. Thinking about it more, reversing the mapping can get quite involved, because it is neither surjective nor injective, so the inverse can actually have any number of results.

In your example there is no input that maps to 0, but there are two inputs that map to 11 (1 and 11). If the seed-to-soil map also included 10 20 2, 21 would also map to 11.

[–] Ategon 5 points 11 months ago* (last edited 11 months ago) (2 children)

[JavaScript] Well that was by far the hardest out of all of the days, part 1 was relatively fine but part 2 took me awhile of trying different things

Ended up solving it by working backwards by trying different location values and seeing if that can become a valid seed. Takes around 3 secs to compute the answer.

Link to code

Part 1 Code Block

// Part 1
// ======

function part1(input) {
  const split = input.split("\r\n\r\n");

  let pastValues = split[0].match(/\d+/g).map((x) => parseInt(x));
  let currentValues = [];

  for (const section of split.slice(1)) {
    for (const line of section.split("\r\n")) {
      const values = line.match(/\d+/g)?.map((x) => parseInt(x));

      if (!values) {
        continue;
      }

      const sourceStart = values[1];
      const destinationStart = values[0];
      const length = values[2];

      for (let i = 0; i < pastValues.length; i++) {
        if (
          pastValues[i] >= sourceStart &&
          pastValues[i] < sourceStart + length
        ) {
          currentValues.push(destinationStart + pastValues[i] - sourceStart);
          pastValues.splice(i, 1);
          i--;
        }
      }
    }

    for (let i = 0; i < pastValues.length; i++) {
      currentValues.push(pastValues[i]);
    }

    pastValues = [...currentValues];
    currentValues = [];
  }

  return Math.min(...pastValues);
}

Part 2 Code Block

// Part 2
// ======

function part2(input) {
  const split = input.split("\r\n\r\n");

  let seeds = split[0].match(/\d+/g).map((x) => parseInt(x));
  seeds = seeds
    .filter((x, i) => i % 2 == 0)
    .map((x, i) => [x, seeds[i * 2 + 1]]);

  const maps = split
    .slice(1)
    .map((x) => {
      const lines = x.split("\r\n");
      return lines
        .map((x) => x.match(/\d+/g)?.map((x) => parseInt(x)))
        .filter((x) => x);
    })
    .reverse();

  for (let i = 0; true; i++) {
    let curValue = i;

    for (const map of maps) {
      for (const line of map) {
        const sourceStart = line[1];
        const destinationStart = line[0];
        const length = line[2];

        if (
          curValue >= destinationStart &&
          curValue < destinationStart + length
        ) {
          curValue = sourceStart + curValue - destinationStart;
          break;
        }
      }
    }

    for (const [seedRangeStart, seedRangeLength] of seeds) {
      if (
        curValue >= seedRangeStart &&
        curValue < seedRangeStart + seedRangeLength
      ) {
        return i;
      }
    }
  }
}

[–] [email protected] 1 points 11 months ago

Torn between doing the problem backwards and implementing a more general case -- glad to know both approaches work out in the end!

[–] [email protected] 1 points 11 months ago (1 children)

Ended up solving it by working backwards by trying different location values and seeing if that can become a valid seed.

Huh, that's clever.

[–] Ategon 1 points 11 months ago

Turns out I got really lucky and my location value is much lower than most peoples which is why it can be solved relatively quickly

[–] kartoffelsaft 4 points 11 months ago* (last edited 11 months ago)

Odin

When I read the problem description I expected the input to also be 2 digit numbers. When I looked at it I just had to say "huh."

Second part I think you definitely have to do in reverse (edit: if you are doing a linear search for the answer), as that allows you to nope out as soon as you find a match, whereas with doing it forward you have to keep checking just in case.

Formatted code

package day5

import "core:fmt"
import "core:strings"
import "core:slice"
import "core:strconv"

Range :: struct {
    dest: int,
    src: int,
    range: int,
}

Mapper :: struct {
    ranges: []Range,
}

parse_range :: proc(s: string) -> (ret: Range) {
    rest := s

    parseLen := -1

    destOk: bool
    ret.dest, destOk = strconv.parse_int(rest, 10, &parseLen)
    rest = strings.trim_left_space(rest[parseLen:])

    srcOk: bool
    ret.src, srcOk = strconv.parse_int(rest, 10, &parseLen)
    rest = strings.trim_left_space(rest[parseLen:])

    rangeOk: bool
    ret.range, rangeOk = strconv.parse_int(rest, 10, &parseLen)

    return
}

parse_mapper :: proc(ss: []string) -> (ret: Mapper) {
    ret.ranges = make([]Range, len(ss)-1)
    for s, i in ss[1:] {
        ret.ranges[i] = parse_range(s)
    }

    return
}

parse_mappers :: proc(ss: []string) -> []Mapper {
    mapsStr := make([dynamic][]string)
    defer delete(mapsStr)

    restOfLines := ss
    isLineEmpty :: proc(s: string)->bool {return len(s)==0}

    for i, found := slice.linear_search_proc(restOfLines, isLineEmpty); 
        found; 
        i, found  = slice.linear_search_proc(restOfLines, isLineEmpty) {
        
        append(&mapsStr, restOfLines[:i])
        restOfLines = restOfLines[i+1:]
    }
    append(&mapsStr, restOfLines[:])

    return slice.mapper(mapsStr[1:], parse_mapper)
}

apply_mapper :: proc(mapper: Mapper, num: int) -> int {
    for r in mapper.ranges {
        if num >= r.src && num - r.src < r.range do return num - r.src + r.dest
    }

    return num
}

p1 :: proc(input: []string) {
    maps := parse_mappers(input)
    defer {
        for m in maps do delete(m.ranges)
        delete(maps)
    }

    restSeeds := input[0][len("seeds: "):]
    min := 0x7fffffff

    for len(restSeeds) > 0 {
        seedLen := -1
        seed, seedOk := strconv.parse_int(restSeeds, 10, &seedLen)
        restSeeds = strings.trim_left_space(restSeeds[seedLen:])

        fmt.print(seed)
        for m in maps {
            seed = apply_mapper(m, seed)
            fmt.print(" ->", seed)
        }
        fmt.println()

        if seed < min do min = seed
    }

    fmt.println(min)
}

apply_mapper_reverse :: proc(mapper: Mapper, num: int) -> int {
    for r in mapper.ranges {
        if num >= r.dest && num - r.dest < r.range do return num - r.dest + r.src
    }

    return num
}

p2 :: proc(input: []string) {
    SeedRange :: struct {
        start: int,
        len: int,
    }

    seeds := make([dynamic]SeedRange)
    restSeeds := input[0][len("seeds: "):]

    for len(restSeeds) > 0 {
        seedLen := -1
        seedS, seedSOk := strconv.parse_int(restSeeds, 10, &seedLen)
        restSeeds = strings.trim_left_space(restSeeds[seedLen:])

        seedL, seedLOk := strconv.parse_int(restSeeds, 10, &seedLen)
        restSeeds = strings.trim_left_space(restSeeds[seedLen:])

        append(&seeds, SeedRange{seedS, seedL})
    }

    maps := parse_mappers(input)
    defer {
        for m in maps do delete(m.ranges)
        delete(maps)
    }

    for i := 0; true; i += 1 {
        rseed := i
        #reverse for m in maps {
            rseed = apply_mapper_reverse(m, rseed)
        }

        found := false
        for sr in seeds {
            if rseed >= sr.start && rseed < sr.start + sr.len {
                found = true
                break
            }
        }
        if found {
            fmt.println(i)
            break
        }
    }
}
[–] [email protected] 4 points 11 months ago

Haskell

Not hugely proud of this one; part one would have been easier if I'd spend more time reading the question and not started on an overly-general solution, and I lost a lot of time on part two to a missing a +. More haste, less speed, eh?

import Data.List
import Data.List.Split

readInput :: String -> ([Int], [(String, [(Int, Int, Int)])])
readInput s =
  let (seedsChunk : mapChunks) = splitOn [""] $ lines s
      seeds = map read $ tail $ words $ head seedsChunk
      maps = map readMapChunk mapChunks
   in (seeds, maps)
  where
    readMapChunk (title : rows) =
      let name = head $ words title
          entries = map ((\[a, b, c] -> (a, b, c)) . map read . words) rows
       in (name, entries)

part1 (seeds, maps) =
  let f = foldl1' (flip (.)) $ map (ref . snd) maps
   in minimum $ map f seeds
  where
    ref [] x = x
    ref ((a, b, c) : rest) x =
      let i = x - b
       in if i >= 0 && i < c
            then a + i
            else ref rest x

mapRange :: [(Int, Int, Int)] -> (Int, Int) -> [(Int, Int)]
mapRange entries (start, end) =
  go start $ sortOn (\(_, b, _) -> b) entries
  where
    go i [] = [(i, end)]
    go i es@((a, b, c) : rest)
      | i > end = []
      | b > end = go i []
      | b + c <= i = go i rest
      | i < b = (i, b - 1) : go b es
      | otherwise =
          let d = min (b + c - 1) end
           in (a + i - b, a + d - b) : go (d + 1) rest

part2 (seeds, maps) =
  let seedRanges = map (\[a, b] -> (a, a + b - 1)) $ chunksOf 2 seeds
   in minimum $ map fst $ foldl' (flip mapRanges) seedRanges $ map snd maps
  where
    mapRanges m = concatMap (mapRange m)

main = do
  input <- readInput <$> readFile "input05"
  print $ part1 input
  print $ part2 input
[–] purplemonkeymad 4 points 11 months ago

Finally. :cries:

Part 1 was fine, I was figuring I might be able to practice classes.

Part 2 told me that nope, memory management required for you. In the end instead of calculating seeds, I resolved the whole thing down to a single mapping of seeds to locations. Then I could just sort by location ranges and try to see if they were a seed. Code is full of old parts from failed solutions but I've had enough of day 5, so I no longer care to clean it up.

[–] [email protected] 3 points 11 months ago (2 children)

Nim

Woof. Part 1 was simple enough. I thought I could adapt my solution to part 2 pretty easily, just add all the values in the ranges to the starting set. Worked fine for the example, but the ranges for the actual input are too large. Ended up taking 16gb of RAM and crunching forever.

I finally abandoned my quick and dirty approach when rewriting part 2, and made some proper types and functions. Treated each range as an object, and used set operations on them. The difference operation tends to fragment the range that it's used on, so I meant to write some code to defragment the ranges after each round of mappings. Forgot to do so, but the code ran quick enough this time anyway.

[–] [email protected] 1 points 11 months ago (1 children)

Treated each range as an object, and used set operations on them

That's smart. Honestly, I don't understand how it works. 😅

The difference operation tends to fragment the range that it’s used on, so I meant to write some code to defragment the ranges after each round of mappings. Forgot to do so, but the code ran quick enough this time anyway.

I've got different solution from yours, but this part is the same, lol. My code slices the ranges into 1-3 parts on each step, so I also planned to 'defragment' them. But performance is plenty without this step, ~450 microseconds for both parts on my PC.

[–] [email protected] 2 points 11 months ago (1 children)

Treated each range as an object, and used set operations on them

That’s smart. Honestly, I don’t understand how it works. 😅

"Set operations" should probably be in quotes. I just mean that I implemented the * (intersection) and - (difference) operators for my ValueRange type. The intersection operator works like it does for sets, just returning the overlap. The difference operator has to work a little differently, because ranges have to be contiguous, whereas sets don't, so it returns a sequence of ValueRange objects.

My ValueMapping type uses a ValueRange for it's source, so applying it to a range just involves using the intersection operator to determine what part of the range needs to move, and the difference operator to determine which parts are left.

[–] [email protected] 1 points 11 months ago* (last edited 11 months ago) (1 children)

Well, then we have the same solution but coded very differently. Here's mine.

ruleApplied is one function with almost all logic. I take a range and compare it to a rule's source range (50 98 2 is a rule). Overlaps get transformed and collected into the first sequence and everything that left goes in the second. I need two seqs there, for transformed values to skip next rules in the same map.

Repeat for each rule and each map (seq[Rule]). And presto, it's working!

[–] [email protected] 2 points 11 months ago (1 children)

Yeah, roughly the same idea. I guess I could have just used HSlice for my range type, I thought maybe there was some special magic to it.

It looks like your if-else ladder misses a corner case, where one range only intersects with the first or last element of the other. Switching to <= and >= for those should take care of it though.

[–] [email protected] 1 points 11 months ago

Thank you, should be fixed now.

[–] [email protected] 1 points 11 months ago

Hi there! Looks like you linked to a Lemmy community using a URL instead of its name, which doesn't work well for people on different instances. Try fixing it like this: [email protected]

[–] cvttsd2si 3 points 11 months ago

Scala3

kind of convoluted, but purely functional

import scala.collection.immutable.NumericRange.Exclusive
import math.max
import math.min

extension [A] (l: List[A])
  def chunk(pred: A => Boolean): List[List[A]] =
    def go(l: List[A], partial_acc: List[A], acc: List[List[A]]): List[List[A]] =
      l match
        case (h :: t) if pred(h) => go(t, List(), partial_acc.reverse :: acc)
        case (h :: t) => go(t, h :: partial_acc, acc)
        case _ => partial_acc.reverse :: acc
    
    go(l, List(), List()).reverse

type R = Exclusive[Long]

def intersectTranslate(r: R, c: R, t: Long): R =
    (t + max(r.start, c.start) - c.start) until (t + min(r.end, c.end) - c.start)

case class MappingEntry(from: R, to: Long)
case class Mapping(entries: List[MappingEntry], produces: String):
    def resolveRange(in: R): List[R] =
        entries.map(e => intersectTranslate(in, e.from, e.to)).filter(!_.isEmpty)

def completeEntries(a: List[MappingEntry]): List[MappingEntry] = 
    a ++ ((0L until 0L) +: a.map(_.from).sorted :+ (Long.MaxValue until Long.MaxValue)).sliding(2).flatMap{ case List(a, b) => Some(MappingEntry(a.end until b.start, a.end)); case _ => None}.toList

def parse(a: List[String], init: List[Long] => List[R]): (List[R], Map[String, Mapping]) =
    def parseEntry(s: String): MappingEntry =
        s match
            case s"$end $start $range" => MappingEntry(start.toLong until start.toLong + range.toLong, end.toLong)

    a.chunk(_ == "") match
        case List(s"seeds: $seeds") :: maps => 
            init(seeds.split(raw"\s+").map(_.toLong).toList) -> (maps.flatMap{ case s"$from-to-$to map:" :: entries => Some(from -> Mapping(completeEntries(entries.map(parseEntry)), to)); case _ => None }).toMap
        case _ => (List(), Map()).ensuring(false)

def singletons(a: List[Long]): List[R] = a.map(s => s until s + 1)
def paired(a: List[Long]): List[R] = a.grouped(2).flatMap{ case List(x, y) => Some(x until x+y); case _ => None }.toList

def chase(d: (List[R], Map[String, Mapping]), initial: String, target: String) =
    val (init, m) = d
    def go(a: List[R], s: String): List[R] =
        if trace(s) == target then a else
            val x = m(s)
            go(a.flatMap(x.resolveRange), x.produces)
    go(trace(init), initial)

def task1(a: List[String]): Long = 
    chase(parse(a, singletons), "seed", "location").min.start

def task2(a: List[String]): Long =
    chase(parse(a, paired), "seed", "location").min.start
[–] bugsmith 3 points 11 months ago* (last edited 11 months ago) (1 children)

Like many others, I really didn't enjoy this one. I particularly struggled with part 02, which ended up with me just brute forcing it and checking each seed. On my system it took over 15 minutes to run, which is truly awful. I'm open to pointers on how I could better have solved part two.

Solution in Rust 🦀

Formatted Solution on GitLab

Code

use std::{
    env, fs,
    io::{self, BufReader, Read},
};

fn main() -> io::Result<()> {
    let args: Vec = env::args().collect();
    let filename = &args[1];
    let file1 = fs::File::open(filename)?;
    let file2 = fs::File::open(filename)?;
    let mut reader1 = BufReader::new(file1);
    let mut reader2 = BufReader::new(file2);

    println!("Part one: {}", process_part_one(&mut reader1));
    println!("Part two: {}", process_part_two(&mut reader2));
    Ok(())
}

#[derive(Debug)]
struct Map {
    lines: Vec,
}

impl Map {
    fn map_to_lines(&self, key: u32) -> u32 {
        for line in &self.lines {
            if line.in_range(key) {
                return line.map(key);
            }
        }
        key
    }
}

#[derive(Debug)]
struct MapLine {
    dest_range: u32,
    source_range: u32,
    range_length: u32,
}

impl MapLine {
    fn map(&self, key: u32) -> u32 {
        let diff = key - self.source_range;
        if self.dest_range as i64 + diff as i64 > 0 {
            return (self.dest_range as i64 + diff as i64) as u32;
        }
        key
    }

    fn in_range(&self, key: u32) -> bool {
        self.source_range <= key
            && (key as i64) < self.source_range as i64 + self.range_length as i64
    }
}

fn parse_input(reader: &amp;mut BufReader) -> (Vec, Vec<map>) {
    let mut almanac = String::new();
    reader
        .read_to_string(&amp;mut almanac)
        .expect("read successful");
    let parts: Vec&lt;&amp;str> = almanac.split("\n\n").collect();
    let (seeds, others) = parts.split_first().expect("at least one part");
    let seeds: Vec&lt;_> = seeds
        .split(": ")
        .last()
        .expect("at least one")
        .split_whitespace()
        .map(|s| s.to_string())
        .collect();
    let maps: Vec&lt;_> = others
        .iter()
        .map(|item| {
            let lines_iter = item
                .split(':')
                .last()
                .expect("exists")
                .trim()
                .split('\n')
                .map(|nums| {
                    let nums_split = nums.split_whitespace().collect::>();
                    MapLine {
                        dest_range: nums_split[0].parse().expect("is digit"),
                        source_range: nums_split[1].parse().expect("is digit"),
                        range_length: nums_split[2].parse().expect("is digit"),
                    }
                });
            Map {
                lines: lines_iter.collect(),
            }
        })
        .collect();
    (seeds, maps)
}

fn process_part_one(reader: &amp;mut BufReader) -> u32 {
    let (seeds, maps) = parse_input(reader);
    let mut res = u32::MAX;
    for seed in &amp;seeds {
        let mut val = seed.parse::().expect("is digits");
        for map in &amp;maps {
            val = map.map_to_lines(val);
        }
        res = u32::min(res, val);
    }
    res
}

fn process_part_two(reader: &amp;mut BufReader) -> u32 {
    let (seeds, maps) = parse_input(reader);
    let seed_chunks: Vec&lt;_> = seeds.chunks(2).collect();
    let mut res = u32::MAX;
    for chunk in seed_chunks {
        let range_start: u32 = chunk[0].parse().expect("is digits");
        let range_length: u32 = chunk[1].parse().expect("is digits");
        let range_end: u32 = range_start + range_length;
        for seed in range_start..range_end {
            let mut val = seed;
            for map in &amp;maps {
                val = map.map_to_lines(val);
            }
            res = u32::min(res, val);
        }
    }
    res
}

#[cfg(test)]
mod tests {
    use super::*;

    const INPUT: &amp;str = "seeds: 79 14 55 13

seed-to-soil map:
50 98 2
52 50 48

soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15

fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4

water-to-light map:
88 18 7
18 25 70

light-to-temperature map:
45 77 23
81 45 19
68 64 13

temperature-to-humidity map:
0 69 1
1 0 69

humidity-to-location map:
60 56 37
56 93 4";

    #[test]
    fn test_process_part_one() {
        let input_bytes = INPUT.as_bytes();
        assert_eq!(35, process_part_one(&amp;mut BufReader::new(input_bytes)));
    }

    #[test]
    fn test_process_part_two() {
        let input_bytes = INPUT.as_bytes();
        assert_eq!(46, process_part_two(&amp;mut BufReader::new(input_bytes)));
    }
}

[–] [email protected] 5 points 11 months ago (1 children)

I got far enough to realize that you probably needed to work backwards and given a location, determine the accompanying seed, and then check if that seed is one of the ones listed in the range. Still though, starting at 0 for location and working up was taking forever to find the first valid seed

Someone in this thread pointed out that if you picked the first value of each range in the map, working backwards from those points will get you a short list of seeds which map to low values. You then check if those seeds are valid, and also check the location of the first seeds in the range (the rest of the seeds in the range don't matter because those are covered by the first check). This ends up with about 200 locations which you can sort, to get the lowest value.

[–] [email protected] 3 points 11 months ago

I tried brute forcing it but couldn't get the process to finish. Iterating through hundreds of millions of seeds is no bueno.

After reading your comment though I got the idea to map whole seed ranges instead of individual seeds. That finished in no time of course.

[–] [email protected] 3 points 11 months ago* (last edited 11 months ago)

This was interesting! So iterating through the solution space would be infeasible here and it seems we need to look for boundaries between regions and follow them to find places where a solution could occur.

Python: https://pastebin.com/8Ckx36fu

  • Make a list of places where location mappings are discontinuous (0, the end of each mapping, and the number before)
  • Repeat this for discontinuities in each intermediate layer
  • Trace each such location to its seed, and filter by seed ranges
  • Run the very minimal set of interesting seed numbers (around 2000 seeds) through the existing part1 algorithm
[–] [email protected] 2 points 11 months ago* (last edited 11 months ago) (1 children)

[C]

My first approach to part 2 was to take the list of ranges, map it to a new list of (possibly split up) ranges, etc, but I realized that would take more memory and bookkeeping than I'd like. Scrapped it and rewrote it with recursion. Much cleaner and the benchmarks are still looking good! (But for how much longer?)

$ bmake bench
day01  0:00.00  1380 Kb  0+78 faults
day02  0:00.00  1660 Kb  0+82 faults
day03  0:00.00  1540 Kb  0+107 faults
day04  0:00.00  1536 Kb  0+80 faults
day05  0:00.00  1668 Kb  0+83 faults

https://github.com/sjmulder/aoc/blob/master/2023/c/day05.c

Edit: I see some people went for looping from 0 to try possible answers. Clever and short but I wanted to go for a more efficient approach.

[–] [email protected] 1 points 11 months ago

Ah, nice! Dealing with each range individually makes things much simpler.

[–] capitalpb 2 points 11 months ago

Well, I can't say much about this one. The code is ugly, horribly inefficient, and part two takes a solid half hour to run. It got the right answer though, so that's something I suppose. I think something like nom to parse the input would be much cleaner, and there's got to be a better way of going about part two than just brute forcing through every possible seed, but hey, it works so that's good enough for now.

https://github.com/capitalpb/advent_of_code_2023/blob/main/src/solvers/day05.rs

#[derive(Clone, Debug)]
struct AlmanacMapEntry {
    destination_range: RangeInclusive,
    source_range: RangeInclusive,
}

#[derive(Clone, Debug)]
struct AlmanacMap {
    entries: Vec,
}

impl AlmanacMap {
    fn from(input: &amp;str) -> AlmanacMap {
        let entries = input
            .lines()
            .skip(1)
            .map(|line| {
                let numbers = line
                    .split(' ')
                    .filter_map(|number| number.parse::().ok())
                    .collect::>();
                AlmanacMapEntry {
                    destination_range: numbers[0]..=(numbers[0] + numbers[2]),
                    source_range: numbers[1]..=(numbers[1] + numbers[2]),
                }
            })
            .collect();
        AlmanacMap { entries }
    }

    fn convert(&amp;self, source: &amp;u64) -> u64 {
        let entry = self
            .entries
            .iter()
            .find(|entry| entry.source_range.contains(&amp;source));

        if let Some(entry) = entry {
            entry.destination_range.start() + (source - entry.source_range.start())
        } else {
            source.clone()
        }
    }
}

#[derive(Debug)]
struct Almanac {
    seeds: Vec,
    seed_to_soil: AlmanacMap,
    soil_to_fertilizer: AlmanacMap,
    fertilizer_to_water: AlmanacMap,
    water_to_light: AlmanacMap,
    light_to_temperature: AlmanacMap,
    temperature_to_humidity: AlmanacMap,
    humidity_to_location: AlmanacMap,
}

impl Almanac {
    fn star_one_from(input: &amp;str) -> Almanac {
        let mut input_sections = input
            .split("\n\n")
            .map(|section| section.split_once(':').unwrap().1);

        let seeds = input_sections
            .next()
            .unwrap()
            .split_whitespace()
            .filter_map(|seed| seed.parse::().ok())
            .collect();

        let almanac_maps = input_sections.map(AlmanacMap::from).collect::>();

        Almanac {
            seeds,
            seed_to_soil: almanac_maps[0].clone(),
            soil_to_fertilizer: almanac_maps[1].clone(),
            fertilizer_to_water: almanac_maps[2].clone(),
            water_to_light: almanac_maps[3].clone(),
            light_to_temperature: almanac_maps[4].clone(),
            temperature_to_humidity: almanac_maps[5].clone(),
            humidity_to_location: almanac_maps[6].clone(),
        }
    }

    fn star_two_from(input: &amp;str) -> Almanac {
        let mut input_sections = input
            .split("\n\n")
            .map(|section| section.split_once(':').unwrap().1);

        let seeds = input_sections
            .next()
            .unwrap()
            .split_whitespace()
            .filter_map(|seed| seed.parse::().ok())
            .collect::>()
            .chunks(2)
            .map(|chunk| (chunk[0]..(chunk[0] + chunk[1])).collect::>())
            .flatten()
            .collect::>();

        let almanac_maps = input_sections.map(AlmanacMap::from).collect::>();

        Almanac {
            seeds,
            seed_to_soil: almanac_maps[0].clone(),
            soil_to_fertilizer: almanac_maps[1].clone(),
            fertilizer_to_water: almanac_maps[2].clone(),
            water_to_light: almanac_maps[3].clone(),
            light_to_temperature: almanac_maps[4].clone(),
            temperature_to_humidity: almanac_maps[5].clone(),
            humidity_to_location: almanac_maps[6].clone(),
        }
    }
}

pub struct Day05;

impl Solver for Day05 {
    fn star_one(&amp;self, input: &amp;str) -> String {
        let almanac = Almanac::star_one_from(input);

        almanac
            .seeds
            .iter()
            .map(|seed| almanac.seed_to_soil.convert(seed))
            .map(|soil| almanac.soil_to_fertilizer.convert(&amp;soil))
            .map(|fertilizer| almanac.fertilizer_to_water.convert(&amp;fertilizer))
            .map(|water| almanac.water_to_light.convert(&amp;water))
            .map(|light| almanac.light_to_temperature.convert(&amp;light))
            .map(|temperature| almanac.temperature_to_humidity.convert(&amp;temperature))
            .map(|humidity| almanac.humidity_to_location.convert(&amp;humidity))
            .min()
            .unwrap()
            .to_string()
    }

    fn star_two(&amp;self, input: &amp;str) -> String {
        let almanac = Almanac::star_two_from(input);

        almanac
            .seeds
            .iter()
            .map(|seed| almanac.seed_to_soil.convert(seed))
            .map(|soil| almanac.soil_to_fertilizer.convert(&amp;soil))
            .map(|fertilizer| almanac.fertilizer_to_water.convert(&amp;fertilizer))
            .map(|water| almanac.water_to_light.convert(&amp;water))
            .map(|light| almanac.light_to_temperature.convert(&amp;light))
            .map(|temperature| almanac.temperature_to_humidity.convert(&amp;temperature))
            .map(|humidity| almanac.humidity_to_location.convert(&amp;humidity))
            .min()
            .unwrap()
            .to_string()
    }
}
[–] [email protected] 2 points 11 months ago* (last edited 11 months ago)

Nim

Part 1 was really easy.
Part 2, I struggled to solve efficiently, so I just ran naive bruteforce for 5 minutes until I got the answer.
Later, I've rewritten my solution for Part 2. The idea is to handle ranges as ranges, check seed ranges against map ranges, transform overlaps, but keep not-overlapping parts.

Total runtime after rewrite: ~ 0.4 ms.
Today's puzzle was nice - 8.5/10.

Code: day_05/solution.nim

[–] [email protected] 2 points 11 months ago

Python

Questions and feedback welcome!

import portion as P

from .solver import Solver

_maps = [
  'seed-to-soil',
  'soil-to-fertilizer',
  'fertilizer-to-water',
  'water-to-light',
  'light-to-temperature',
  'temperature-to-humidity',
  'humidity-to-location',
]

def group_lines_in_maps(lines):
  group = []
  for line in lines:
    if not line:
      yield group
      group = []
      continue
    group.append(line)
  yield group

class Day05(Solver):
  def __init__(self):
    super().__init__(5)
    self.seeds = []
    self.mappings = {}

  def presolve(self, input: str):
    lines = input.rstrip().split('\n')
    self.seeds = list(map(int, lines[0].split(' ')[1:]))
    self.mappings = {}
    for mapping in group_lines_in_maps(lines[2:]):
      mapping_name = mapping[0].split(' ')[0]
      mapping_ranges = map(lambda rng: tuple(map(int, rng.split(' '))), mapping[1:])
      self.mappings[mapping_name] = list(mapping_ranges)


  def solve_first_star(self):
    locations = []
    for seed in self.seeds:
      location = seed
      for mapping in map(self.mappings.get, _maps):
        assert mapping
        for dest, source, length in mapping:
          if 0 &lt;= location - source &lt; length:
            location = dest + (location - source)
            break
      locations.append(location)
    return min(locations)


  def solve_second_star(self):
    current_set = P.empty()
    for i in range(0, len(self.seeds), 2):
      current_set = current_set | P.closedopen(self.seeds[i], self.seeds[i] + self.seeds[i + 1])
    for mapping in map(self.mappings.get, _maps):
      assert mapping
      unmapped = current_set
      next_set = P.empty()
      for dest, source, length in mapping:
        delta = dest - source
        source_range = P.closedopen(source, source + length)
        mappable = unmapped &amp; source_range
        mapped_to_destination = mappable.apply(
            lambda x: (x.left, x.lower + delta, x.upper + delta, x.right))  # pylint: disable=cell-var-from-loop
        next_set = next_set | mapped_to_destination
        unmapped = unmapped - source_range
      current_set = next_set | unmapped
    return next(P.iterate(current_set, step=1))
[–] [email protected] 2 points 11 months ago (1 children)

Honestly this one was quite easy for me. Not so much for my computer though...

https://git.sr.ht/~aidenisik/aoc23/tree/master/item/day5

C solutions. Disclaimer, part 2 has not finished running. But it's mostly the same code as part 1 and it works on the small sample data so it'll be fine.

[–] [email protected] 2 points 11 months ago (1 children)

Disclaimer, part 2 has not finished running. But it’s mostly the same code as part 1 and it works on the small sample data so it’ll be fine.

RIP?

[–] [email protected] 2 points 11 months ago

Still running :)

I don't have the most powerful hardware unfortunately

[–] [email protected] 2 points 11 months ago

CRYSTAL

finally solved part 2, and in just 1 second :)))

Input = File.read("input.txt").lines

# {source, destination}
alias Map = Tuple(Range(Int64, Int64), Range(Int64, Int64))

Maps = Array(Array(Map)).new(7)
index = 1
7.times do |i| 
	a, index = get_ranges(index + 2)
	Maps &lt;&lt; a
end

part2
# part1

def part1()
	seeds = Input[0].split(":")[1].split.map(&amp;.to_i64)
	locs = Array(Int64).new(7)
	
	seeds.each do |seed|
		val = seed
		Maps.each do |maplist|
			maplist.each do |map|
				if map[0].includes?(val)
					val = map[1].begin + (val - map[0].begin)
					break
		end     end     end
		locs &lt;&lt; val
	end
	puts locs.min
end

def part2()
	seeds = Input[0].split(":")[1].split.map(&amp;.to_i64)
	seeds = seeds.in_groups_of(2, 0).map { |a| a[0]..(a[0]+a[1]) }

	found = false
	loc = 0
	until found 
		val = loc
		Maps.reverse_each do |maplist|
			maplist.each do |map|
				if map[1].includes?(val)
					val = map[0].begin + (val - map[1].begin)
					break
		end     end     end

		seeds.each { |seedrange| break if found = seedrange.includes?(val) }
		loc += 1
	end
	puts loc - 1
end

def get_ranges(index : Int) : {Array(Map), Int32}
	line = Input[index]
	ranges = [] of Map
	until line == ""
		a, b, l = line.split.map(&amp;.to_i64)
		ranges &lt;&lt; {b...(b+l), a...(a+l)}
		
		index += 1
		break if index == Input.size
		line = Input[index]
	end
	{ranges, index}
end
[–] [email protected] 2 points 11 months ago* (last edited 11 months ago) (1 children)

[Language: Lean4]

I'll only post the actual parsing and solution. I have written some helpers (in this case particularly relevant: Quicksort) which are in other files, as is the main function. For the full code, please see my github repo.

This one also ended up quite long, because I couldn't resist to use different types for the different things, and to have the type checker confirm that I'm combining the maps between them in the correct order.

Also, I am not 100% certain that part 2 doesn't have any off-by-one errors. I didn't write any unit tests for it... The answer is correct though, so I probably didn't mess it up too horribly. Also, it is pretty fast. Part 2 takes about 1.2 milliseconds on my machine, and this is including the file parsing (but not the loading of the file).

It seems my solution is too long for a single post though, so I'll split off part 2 and post it separately.

Edit: There was a bug in the function that checks overlaps between ranges while parsing.

Parsing and Part 1

structure Seed where
  id : Nat
  deriving BEq, Ord, Repr

structure Soil where
  id : Nat
  deriving BEq, Ord, Repr

structure Fertilizer where
  id : Nat
  deriving BEq, Ord, Repr

structure Water where
  id : Nat
  deriving BEq, Ord, Repr

structure Light where
  id : Nat
  deriving BEq, Ord, Repr

structure Temperature where
  id : Nat
  deriving BEq, Ord, Repr

structure Humidity where
  id : Nat
  deriving BEq, Ord, Repr

structure Location where
  id : Nat
  deriving BEq, Ord, Repr

private class NatId (α : Type) where
  fromNat : Nat → α
  toNat : α → Nat

private instance : NatId Seed where
  fromNat := Seed.mk
  toNat := Seed.id

private instance : NatId Soil where
  fromNat := Soil.mk
  toNat := Soil.id

private instance : NatId Fertilizer where
  fromNat := Fertilizer.mk
  toNat := Fertilizer.id

private instance : NatId Water where
  fromNat := Water.mk
  toNat := Water.id

private instance : NatId Light where
  fromNat := Light.mk
  toNat := Light.id

private instance : NatId Temperature where
  fromNat := Temperature.mk
  toNat := Temperature.id

private instance : NatId Humidity where
  fromNat := Humidity.mk
  toNat := Humidity.id

private instance : NatId Location where
  fromNat := Location.mk
  toNat := Location.id

private instance : Min Location where
  min a b := if Ord.compare a b == Ordering.lt then a else b

structure Mapping (α β : Type) where
  inputStart : α
  outputStart : β
  length : Nat
  deriving Repr

structure Mappings (α β : Type) where
  mappings : List $ Mapping α β
  deriving Repr

private def Mapping.apply? {α β : Type} [NatId α] [NatId β] (mapping : Mapping α β) (input : α) : Option β :=
  let input := NatId.toNat input
  let fromStart := NatId.toNat mapping.inputStart
  let toStart := NatId.toNat mapping.outputStart
  if input ≥ fromStart ∧ input &lt; fromStart + mapping.length then
    some $ NatId.fromNat $ toStart + input - fromStart
  else
    none

private def Mappings.apply {α β : Type} [NatId α] [NatId β] (mappings : Mappings α β) (input : α) : β :=
  let applied := mappings.mappings.findSome? $ flip Mapping.apply? input
  applied.getD $ NatId.fromNat $ NatId.toNat input

structure Almanach where
  seedsToSoil : Mappings Seed Soil
  soilToFertilizer : Mappings Soil Fertilizer
  fertilizerToWater : Mappings Fertilizer Water
  waterToLight : Mappings Water Light
  lightToTemperature : Mappings Light Temperature
  temperatureToHumidity : Mappings Temperature Humidity
  humidityToLocation : Mappings Humidity Location
  deriving Repr

private def parseSeeds (input : String) : Option (List Seed) :=
  if input.startsWith "seeds: " then
    let input := input.drop 7
    let input := String.trim &lt;$> input.split Char.isWhitespace
    let numbers := input.mapM String.toNat?
    List.map NatId.fromNat &lt;$> numbers
  else
    none

private def parseMapping {α β : Type} [NatId α] [NatId β] (input : String) : Option $ Mapping α β := do
  let input := String.trim &lt;$> input.split Char.isWhitespace
  let nums ← input.mapM String.toNat?
  match nums with
  | [a,b,c] => some $ {inputStart := NatId.fromNat b, outputStart := NatId.fromNat a, length := c}
  | _ => none

private def Mapping.overlap {α β : Type} [NatId α] [NatId β] (a : Mapping α β) (b : Mapping α β) : Bool :=
  let aStart := NatId.toNat $ a.inputStart
  let aEnd := aStart + a.length
  let bStart := NatId.toNat $ b.inputStart
  let bEnd := bStart + b.length
  (bStart ≥ aStart &amp;&amp; bStart &lt; aEnd)
   || (bEnd > aStart &amp;&amp; bEnd ≤ aEnd)
   || (aStart ≥ bStart &amp;&amp; aStart &lt; bEnd)
   || (aEnd > bStart &amp;&amp; aEnd ≤ bEnd)

private def parseMappings (α β : Type) [NatId α] [NatId β] (input : String) (header : String) : Option $ Mappings α β :=
  if input.startsWith header then
    let lines := String.trim &lt;$> input.splitOn "\n" |> List.drop 1 |> List.filter (not ∘ String.isEmpty)
    let mappings := lines.mapM parseMapping
    let rec overlapHelper := λ (a : List $ Mapping α β) ↦ match a with
      | [] => false
      | a :: as => as.any (λ b ↦ a.overlap b) || overlapHelper as
    let mappings := mappings.filter $ not ∘ overlapHelper --make sure no ranges overlap. That would be faulty
    Mappings.mk &lt;$> mappings
  else
   none

def parse (input : String) : Option ((List Seed) × Almanach) := do
  let blocks := input.splitOn "\n\n" |> List.filter (not ∘ String.isEmpty)
  let blocks := String.trim &lt;$> blocks
  if let [seeds, seedToSoil, soilToFertilizer, fertilizerToWater, waterToLight, lightToTemperature, temperatureToHumidity, humidityToLocation] := blocks then
    let seeds ← parseSeeds seeds
    let seedToSoil ← parseMappings Seed Soil seedToSoil "seed-to-soil map:"
    let soilToFertilizer ← parseMappings Soil Fertilizer soilToFertilizer "soil-to-fertilizer map:"
    let fertilizerToWater ← parseMappings Fertilizer Water fertilizerToWater "fertilizer-to-water map:"
    let waterToLight ← parseMappings Water Light waterToLight "water-to-light map:"
    let lightToTemperature ← parseMappings Light Temperature lightToTemperature "light-to-temperature map:"
    let temperatureToHumidity ← parseMappings Temperature Humidity temperatureToHumidity "temperature-to-humidity map:"
    let humidityToLocation ← parseMappings Humidity Location humidityToLocation "humidity-to-location map:"
    (seeds, {
      seedsToSoil := seedToSoil
      soilToFertilizer := soilToFertilizer
      fertilizerToWater := fertilizerToWater
      waterToLight := waterToLight
      lightToTemperature := lightToTemperature
      temperatureToHumidity := temperatureToHumidity
      humidityToLocation := humidityToLocation
    : Almanach})
  else
    none

def part1 (input : ((List Seed) × Almanach)) : Option Nat :=
  let a := input.snd
  let seedToLocation  := a.humidityToLocation.apply
                      ∘ a.temperatureToHumidity.apply
                      ∘ a.lightToTemperature.apply
                      ∘ a.waterToLight.apply
                      ∘ a.fertilizerToWater.apply
                      ∘ a.soilToFertilizer.apply
                      ∘ a.seedsToSoil.apply
  let locations := input.fst.map seedToLocation
  NatId.toNat &lt;$> locations.minimum?

[–] [email protected] 1 points 11 months ago* (last edited 11 months ago)

Part 2

private structure Mapping2 (α β : Type) where
  start : α --okay, next time I do this, I'll encode end and offset, not start and offset...
  offset : Int
  deriving Repr

private structure Mappings2 (α β : Type) where
  mappings : List $ Mapping2 α β
  deriving Repr

private def Mappings2.fromMappings {α β : Type} [NatId α] [NatId β] [Ord α] (input : Mappings α β) : Mappings2 α β :=
  let input := input.mappings.quicksortBy λ a b ↦ (Ord.compare a.inputStart b.inputStart == Ordering.lt)
  let rec helper := λ
    | [] => []
    | a :: [] => [{ start:= a.inputStart, offset := (NatId.toNat a.outputStart) - (NatId.toNat a.inputStart)},
                 {start:= NatId.fromNat (NatId.toNat a.inputStart + a.length), offset := 0}]
    | a :: b :: as => if (NatId.toNat b.inputStart) != (NatId.toNat a.inputStart + a.length) then
                        { start:= a.inputStart, offset := (NatId.toNat a.outputStart) - (NatId.toNat a.inputStart)}
                        :: { start:= NatId.fromNat (NatId.toNat a.inputStart + a.length), offset := 0}
                        :: helper (b :: as)
                      else
                        { start:= a.inputStart, offset := (NatId.toNat a.outputStart) - (NatId.toNat a.inputStart)}
                        :: helper (b :: as)
  let result := match input with
    | [] => []
    | a :: _ =>  if NatId.toNat a.inputStart != 0 then
                    { start:= NatId.fromNat 0, offset := 0 : Mapping2 α β} :: helper input
                  else
                    helper input
  Mappings2.mk result

private def Mappings2.apply (α β : Type) [NatId α] [NatId β] [Ord α] (mapping : Mappings2 α β) (value : α) : β :=
  let rec findOffsetHelper := λ
    | [] => 0
    | a :: [] => a.offset
    | a :: b :: as => if (Ord.compare value b.start == Ordering.lt) then a.offset else findOffsetHelper (b :: as)
  let offset : Int := findOffsetHelper mapping.mappings
  let result : Int := (NatId.toNat value + offset)
  NatId.fromNat result.toNat

private def Mappings2.combine {α β γ : Type} [NatId α] [NatId β] [NatId γ] (a : Mappings2 α β) (b : Mappings2 β γ) : Mappings2 α γ :=
  -- at this point, let's just go integer
  let a : List (Int × Int) := a.mappings.map λ m ↦ (NatId.toNat m.start, m.offset)
  let b : List (Int × Int):= b.mappings.map λ m ↦ (NatId.toNat m.start, m.offset)
  let rec findOffsetHelper := λ (list : List (Int × Int)) (value : Int) ↦ match list with
    | [] => 0
    | a :: [] => a.snd
    | a :: b :: as => if (value &lt; b.fst) then a.snd else findOffsetHelper (b :: as) value

  let rec helper := λ
    | [] => b
    | a :: [] =>
      let bOffsetAtA := findOffsetHelper b (a.fst + a.snd)
      let bRemainder := b.dropWhile (λ (bb : Int × Int) ↦ a.fst + a.snd > bb.fst)
      match bRemainder with
        | [] => [(a.fst, a.snd + bOffsetAtA)]
        | b :: _ =>  if b.fst - a.snd == a.fst then
                        bRemainder.map λ (b : Int × Int) ↦ (b.fst - a.snd, a.snd + b.snd)
                      else
                        (a.fst, a.snd + bOffsetAtA) :: bRemainder.map λ (b : Int × Int) ↦ (b.fst - a.snd, a.snd + b.snd)
    | a :: aa :: as =>
      let bOffsetAtA := findOffsetHelper b (a.fst + a.snd)
      let relevantBs := b.filter (λ (bb : Int × Int) ↦ a.fst + a.snd ≤ bb.fst &amp;&amp; aa.fst + a.snd > bb.fst)
      match relevantBs with
        | [] => (a.fst, a.snd + bOffsetAtA) :: (helper (aa :: as))
        | b :: _ =>  if b.fst - a.snd == a.fst then
                        (relevantBs.map λ (b : Int × Int) ↦ (b.fst - a.snd, a.snd + b.snd))
                        ++ helper (aa :: as)
                      else
                        (a.fst, a.snd + bOffsetAtA) :: (relevantBs.map λ (b : Int × Int) ↦ (b.fst - a.snd, a.snd + b.snd))
                        ++ helper (aa :: as)
  let result := helper a
  Mappings2.mk $ result.map λ p ↦ { start := NatId.fromNat p.fst.toNat, offset := p.snd : Mapping2 α γ}

private structure SeedRange where
  start : Seed
  ending : Seed
  deriving Repr

private def SeedRange.fromList (input : List Seed) : List SeedRange :=
  let rec helper : List Seed → List SeedRange := λ
    | [] => []
    | _ :: [] => []
    | a :: b :: as => { start := a, ending := Seed.mk $ b.id + a.id} :: SeedRange.fromList as
  (helper input).quicksortBy λ a b ↦ a.start.id &lt; b.start.id

private def SeedRange.findSmallestSeedAbove (seedRanges : List SeedRange) (value : Seed) : Option Seed :=
  -- two options: If the value is inside a seedRange, the value itself is the result
  --              If not, the start of the first seedRange above the value is the result
  let rangeContains := λ r ↦ (Ord.compare r.start value != Ordering.gt) &amp;&amp; (Ord.compare r.ending value == Ordering.gt)
  let rec helper := λ
  | [] => none
  | r :: rs =>  if rangeContains r then
                  some value
                else
                  if Ord.compare r.start value == Ordering.gt then
                    r.start
                  else
                    helper rs
  helper seedRanges

def part2 (input : ((List Seed) × Almanach)) : Option Nat :=
  let a := input.snd
  let seedToLocation := Mappings2.fromMappings a.seedsToSoil
    |> flip Mappings2.combine (Mappings2.fromMappings a.soilToFertilizer)
    |> flip Mappings2.combine (Mappings2.fromMappings a.fertilizerToWater)
    |> flip Mappings2.combine (Mappings2.fromMappings a.waterToLight)
    |> flip Mappings2.combine (Mappings2.fromMappings a.lightToTemperature)
    |> flip Mappings2.combine (Mappings2.fromMappings a.temperatureToHumidity)
    |> flip Mappings2.combine (Mappings2.fromMappings a.humidityToLocation)

  let seedRanges := SeedRange.fromList input.fst

  let potentialSeeds := seedToLocation.mappings.filterMap λ m ↦
    (SeedRange.findSmallestSeedAbove seedRanges m.start) -- could filter by range end, but who cares?
  let locations := potentialSeeds.map seedToLocation.apply
  NatId.toNat &lt;$> locations.minimum?

[–] [email protected] 2 points 11 months ago* (last edited 11 months ago) (1 children)
[–] [email protected] 1 points 11 months ago

Started 4 days late so coming up from behind. Day 5 was the first solution I am somewhat proud of. I used interval arithmetics. I had to somewhat extend a class interval from pyinterval into something I called PointedInterval. In the end part 2 was completed in 0.32 seconds. It does not reverse engineer the solution starting from 0 location and inverse mapping until you find a seed (that was how I was initially planning to do it). It maps forward everything as intervals. There is a bit of a boiler plate which is in the utils file.

[–] Massahud 2 points 11 months ago

Language: Python

Github

Catching up missed days.

[–] [email protected] 1 points 11 months ago

[RUST] https://codeberg.org/Sekoia/adventofcode/src/branch/main/src/y2023/day5.rs

Rank 276 on part 2, personal best for me! I spent too much time struggling with parsing, the lib I'm using doesn't have an easy way to just throw out some values, so I had to filter out the first line of sections. Part 1 was relatively straightforward once parsed though, and part 2 I knew what I had to do, I just didn't want to. But the naïve way is... several orders of magnitude too slow.

[–] [email protected] 1 points 11 months ago* (last edited 11 months ago)

Language: Python

Part 1

The first part wasn't too bad... once I realized you should store the ranges and not actually generate all the numbers o_O.

Seeds = list[int]
Map   = tuple[int, int, int]
Maps  = list[Map]

def read_almanac(stream=sys.stdin) -> tuple[Seeds, list[Map]]:
    seeds: Seeds = [int(seed) for seed in stream.readline().split(':')[-1].split()]
    maps:  Maps  = []

    for line in map(str.strip, stream):
        if line.endswith('map:'):
            maps.append([])
            continue
            
        try:
            dst, src, length = map(int, line.split())
        except ValueError:
            continue
            
        maps[-1].append((dst, src, length))

    return seeds, maps

def locate_seed(seed: int, maps: list[Map]) -> int:
    location = seed
    for amap in maps:
        for dst, src, length in amap:
            if src &lt;= location &lt; src + length:
                location = dst + (location - src)
                break
    return location

def main(stream=sys.stdin) -> None:
    seeds, maps = read_almanac(stream)
    locations   = [locate_seed(seed, maps) for seed in seeds]
    print(min(locations))

Part 2

This part took me forever, mostly to actually run, but also to fix a few off-by-one errors :|

Fortunately, I was able to re-use most of my code in Part A and just add a new function to search the range of seeds.

Even with concurrent.futures and a 24-core machine, it still took me about 30 - 45 minutes to complete with Python (that said, there were only 10 seed range s in the input, so I could only use 10 cores, and of those 5 of the ranges appeared to be really large, leading to a long tail effect).

def locate_seeds(srange: Range, maps: list[Map]={}) -> int:
    seeds   = range(srange[0], srange[0] + srange[1])
    locator = functools.partial(locate_seed, maps=maps)
    return min(map(locator, seeds))

# Main Execution

def main(stream=sys.stdin) -> None:
    seeds, maps = read_almanac(stream)
    locator     = functools.partial(locate_seeds, maps=maps)

    with concurrent.futures.ProcessPoolExecutor() as executor:
        locations = executor.map(locator, batched(seeds, 2))

    print(min(locations))

GitHub Repo

[–] [email protected] 1 points 11 months ago

[Rust]

Part 2

use std::fs;
use std::path::PathBuf;

use clap::Parser;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    input_file: PathBuf,
}

/// Start &lt; end
#[derive(Debug, Copy, Clone)]
struct Range {
    start: Idx,
    end: Idx,
}

impl From> for Range {
    fn from(item: std::ops::Range) -> Self {
        Range {
            start: item.start,
            end: item.end,
        }
    }
}

impl std::ops::Add for Range {
    type Output = Self;

    fn add(self, offset: i64) -> Self {
        Range {
            start: self.start + offset,
            end: self.end + offset,
        }
    }
}

#[derive(Debug, Copy, Clone)]
enum Value {
    Unmapped(Range),
    Mapped(Range),
}

impl Value {
    fn value(self) -> Range {
        match self {
            Self::Mapped(n) => n,
            Self::Unmapped(n) => n,
        }
    }

    fn min(self) -> i64 {
        self.value().start
    }
}

fn main() {
    // Parse CLI arguments
    let cli = Cli::parse();

    // Read file
    let input_text = fs::read_to_string(&amp;cli.input_file)
        .expect(format!("File \"{}\" not found", cli.input_file.display()).as_str());

    println!("{}", input_text);

    // Split into seeds and individual maps
    let mut maps_text = input_text.split("\n\n");
    let seeds_text = maps_text.next().expect("Seeds");

    // Seed numbers; will be mapped to the final locations
    let seed_numbers: Vec = parse_seeds(seeds_text);
    let seed_numbers = parse_seed_ranges(seed_numbers);
    let mut seed_values: Vec = seed_numbers.iter().map(|n| Value::Unmapped(*n)).collect();

    eprintln!("Seeds: {:?}", seed_values);

    // Apply maps to seeds
    for map_text in maps_text {
        let mut map_ranges_text = map_text.lines();
        let map_description = map_ranges_text
            .next()
            .expect("There should be a description of the map here");
        eprintln!("Map: {}", map_description);

        // Apply ranges to seeds
        // Map structure:
        // dest start, source start, range length
        for range_text in map_ranges_text {
            let range_numbers: Vec = range_text
                .split_ascii_whitespace()
                .map(|n| n.parse().expect("Should be a number here"))
                .collect();

            eprintln!("\trange: {:?}", range_numbers);

            let source_range = range_numbers[1]..range_numbers[1] + range_numbers[2];
            let offset = range_numbers[0] - range_numbers[1];

            eprintln!("\t\tSource range: {:?}\tOffset: {}", source_range, offset);

            // Apply the range map to the seeds
            seed_values = seed_values
                .iter()
                .map(|n| match n {
                    Value::Unmapped(n) => map_seed_range(*n, source_range.clone().into(), offset),
                    Value::Mapped(_) => vec![*n],
                })
                .flatten()
                .collect();

            eprintln!("\t\tSeed values: {:?}", seed_values);
        }

        // Reset seed values to unmapped
        seed_values = seed_values
            .iter()
            .map(|v| Value::Unmapped(v.value()))
            .collect();
    }

    // Find minimum
    let min_location = seed_values.iter().map(|v| v.min()).min().unwrap();

    println!();
    println!("Part 2: {}", min_location);
}

/// Parse string to vec of string numbers
fn parse_seeds(seeds_text: &amp;str) -> Vec {
    seeds_text
        .split_ascii_whitespace()
        .skip(1)
        .map(|n| n.parse().expect("Should be a number here"))
        .collect()
}

/// Fill out ranges of seed numbers
fn parse_seed_ranges(seed_numbers: Vec) -> Vec> {
    seed_numbers
        .chunks(2)
        .map(|v| (v[0]..v[0] + v[1]).into())
        .collect()
}

/// Maps a seed range to possibly multiple based on a source range and offset
fn map_seed_range(seed_range: Range, source_range: Range, offset: i64) -> Vec {
    let start_cmp = seed_range.start &lt; source_range.start;
    let end_cmp = seed_range.end &lt;= source_range.end;

    match (start_cmp, end_cmp) {
        (false, false) => {
            if source_range.end &lt;= seed_range.start {
                vec![Value::Unmapped(seed_range)]
            } else {
                vec![
                    Value::Mapped((seed_range.start + offset..source_range.end + offset).into()),
                    Value::Unmapped((source_range.end..seed_range.end).into()),
                ]
            }
        }
        (false, true) => vec![Value::Mapped(seed_range + offset)],
        (true, false) => vec![
            Value::Unmapped((seed_range.start..source_range.start).into()),
            Value::Mapped((source_range.start + offset..source_range.end + offset).into()),
            Value::Unmapped((source_range.end..seed_range.end).into()),
        ],
        (true, true) => {
            if seed_range.end &lt;= source_range.start {
                vec![Value::Unmapped(seed_range)]
            } else {
                vec![
                    Value::Unmapped((seed_range.start..source_range.start).into()),
                    Value::Mapped((source_range.start + offset..seed_range.end + offset).into()),
                ]
            }
        }
    }
}