13 votes

Day 5: If You Give A Seed A Fertilizer

Today's problem description: https://adventofcode.com/2023/day/5

Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):

<details>
<summary>Part 1</summary>

```python
Your code here.
```

</details>

33 comments

  1. jackson
    Link
    This one was tricky! I was hoping to make top 1000 for at least one problem, and I wound up getting it on part 1 today. Apologies in advance for the bad variable names, ha. My pt2 solution took...

    This one was tricky! I was hoping to make top 1000 for at least one problem, and I wound up getting it on part 1 today.

    Apologies in advance for the bad variable names, ha.

    My pt2 solution took 8m44s to run, it's not a "full" bruteforce in the context of the problem but it certainly is a bruteforce. I'll have to look at some other solutions tomorrow and see what people have done to get a really well-optimized program.

    Part 1

    Saving the ranges of each map into a dict then itering over seeds.

    def make_map(entries: "list[str]") -> "dict[tuple[int, int], int]":
      out = {}
      for entry in entries:
        items = [int(z) for z in entry.split(" ")]
        dest = items[0]
        src = items[1]
        rng = items[2]
    
        out[(src, src + rng)] = dest
      
      return out
    
    def get_seeds(line):
      l = line.replace("seeds: ", "")
      return [int(i) for i in l.split(" ")]
    
    def find_or_default(m: "dict[tuple[int, int], int]", t: int) -> int:
      for k in m.keys():
        if k[0] <= t <= k[1]:
          return m[k] + t - k[0]
      
      return t
    
    with open("day5.txt") as f:
      data = f.read()
      seeds = get_seeds(data.split("\n")[0])
      print(seeds)
    
      zones = data.split("\n\n")[1:]
    
      maps = []
    
      for zone in zones:
        maps.append(make_map(zone.split("\n")[1:]))
    
      d = []
      for s in seeds:
        last = s
        for m in maps:
          last = find_or_default(m, last)
        d.append(last)
        
      print(min(d))
    
    Part 2

    Working backwards; starting at 1 checks every number to see if, starting from the location, the output is a valid seed.

    def make_map(entries: "list[str]") -> "dict[tuple[int, int], int]":
      out = {}
      for entry in entries:
        items = [int(z) for z in entry.split(" ")]
        dest = items[0]
        src = items[1]
        rng = items[2] - 1
    
        out[(dest, dest + rng)] = src
      
      return out
    
    def get_seeds(line):
      l = [int(i) for i in line.replace("seeds: ", "").split(" ")]
      return [(l[x], l[x+1]) for x in range(0, len(l), 2)]
    
    def is_seed(seeds, t) -> bool:
      for pair in seeds:
        if pair[0] <= t <= pair[0] + pair[1] - 1:
          return True
      return False
    
    def find_or_default(m: "dict[tuple[int, int], int]", t: int) -> int:
      for k in m.keys():
        if k[0] <= t <= k[1]:
          return m[k] + t - k[0]
      
      return t
    
    with open("day5.txt") as f:
      data = f.read()
      seeds = get_seeds(data.split("\n")[0].split("seeds: ")[1])
    
      zones = data.split("\n\n")[1:]
    
      maps = [make_map(zone.split("\n")[1:]) for zone in zones]
    
      for x in range(1, 1000000000):
        if x % 10000 == 0:
          print(x)
        last = x
        for m in maps[::-1]:
          last = find_or_default(m, last)
        if is_seed(seeds, last):
          print("DONE", x)
          break
    
    3 votes
  2. [3]
    DataWraith
    Link
    Part 1 was simple, but completing part 2 was a nightmare because I wasn't willing to brute-force the solution, and the algorithm I had in mind for computing the mappings somehow didn't work on the...

    Part 1 was simple, but completing part 2 was a nightmare because I wasn't willing to brute-force the solution, and the algorithm I had in mind for computing the mappings somehow didn't work on the actual puzzle input for the longest time. This is the fourth time in five problems that I've had code that solved the test input, but not the actual input.

    If the difficulty gets even higher, I'll have to abort much sooner than I had hoped. :(

    (Rust)

    Parser
    use nom::{
        bytes::complete::tag,
        character::complete::{digit1, newline, space1},
        multi::{many0, many1, separated_list1},
        IResult,
    };
    
    #[derive(Debug, PartialEq, Eq, Hash, Clone)]
    pub struct Almanac {
        pub seeds: Vec<usize>,
        pub maps: Vec<Vec<RangeMap>>,
    }
    
    #[derive(Debug, PartialEq, Eq, Hash, Clone)]
    pub struct RangeMap {
        pub destination_range_start: usize,
        pub source_range_start: usize,
        pub range_length: usize,
    }
    
    pub fn parse(input: &str) -> IResult<&str, Almanac> {
        let (input, seeds) = parse_seeds(input)?;
        let (input, _) = tag("seed-to-soil map:\n")(input)?;
        let (input, seed_to_soil_map) = parse_map(input)?;
        let (input, _) = tag("soil-to-fertilizer map:\n")(input)?;
        let (input, soil_to_fertilizer_map) = parse_map(input)?;
        let (input, _) = tag("fertilizer-to-water map:\n")(input)?;
        let (input, fertilizer_to_water_map) = parse_map(input)?;
        let (input, _) = tag("water-to-light map:\n")(input)?;
        let (input, water_to_light_map) = parse_map(input)?;
        let (input, _) = tag("light-to-temperature map:\n")(input)?;
        let (input, light_to_temperature_map) = parse_map(input)?;
        let (input, _) = tag("temperature-to-humidity map:\n")(input)?;
        let (input, temperature_to_humidity_map) = parse_map(input)?;
        let (input, _) = tag("humidity-to-location map:\n")(input)?;
        let (input, humidity_to_location_map) = parse_map(input)?;
    
        Ok((
            input,
            Almanac {
                seeds,
                maps: vec![
                    seed_to_soil_map,
                    soil_to_fertilizer_map,
                    fertilizer_to_water_map,
                    water_to_light_map,
                    light_to_temperature_map,
                    temperature_to_humidity_map,
                    humidity_to_location_map,
                ],
            },
        ))
    }
    
    fn parse_usize(input: &str) -> IResult<&str, usize> {
        let (input, num) = digit1(input)?;
        let num = num.parse::<usize>().unwrap();
    
        Ok((input, num))
    }
    
    fn parse_seeds(input: &str) -> IResult<&str, Vec<usize>> {
        let (input, _) = tag("seeds: ")(input)?;
        let (input, seeds) = separated_list1(space1, parse_usize)(input)?;
        let (input, _) = many1(newline)(input)?;
    
        Ok((input, seeds))
    }
    
    fn parse_map(input: &str) -> IResult<&str, Vec<RangeMap>> {
        let (input, map) = separated_list1(newline, parse_range)(input)?;
        let (input, _) = many0(newline)(input)?;
    
        Ok((input, map))
    }
    
    fn parse_range(input: &str) -> IResult<&str, RangeMap> {
        let (input, destination_range_start) = parse_usize(input)?;
        let (input, _) = space1(input)?;
        let (input, source_range_start) = parse_usize(input)?;
        let (input, _) = space1(input)?;
        let (input, range_length) = parse_usize(input)?;
    
        Ok((
            input,
            RangeMap {
                destination_range_start,
                source_range_start,
                range_length,
            },
        ))
    }
    
    Part 1 & 2
    #![feature(array_chunks)]
    
    use std::{collections::VecDeque, ops::Range};
    
    mod parser;
    use parser::{Almanac, RangeMap};
    
    fn main() {
        println!("Part 1: {}", part1(include_str!("../input.txt")));
        println!("Part 2: {}", part2(include_str!("../input.txt")));
    }
    
    impl RangeMap {
        fn lookup(&self, number: usize) -> usize {
            let source_range = self.source_range_start..(self.source_range_start + self.range_length);
    
            if !source_range.contains(&number) {
                return number;
            }
    
            let idx = number - self.source_range_start;
    
            self.destination_range_start + idx
        }
    
        fn range_split(&self, other_range: Range<isize>) -> Vec<Range<isize>> {
            fn range_intersects(r1: &Range<isize>, r2: &Range<isize>) -> bool {
                r1.start >= r2.start && r1.start < r2.end || r2.start >= r1.start && r2.start < r1.end
            }
    
            let my_range = (self.source_range_start as isize)
                ..((self.source_range_start + self.range_length) as isize);
    
            if !range_intersects(&my_range, &other_range) {
                return vec![other_range];
            }
    
            let mut result = Vec::with_capacity(3);
    
            // Left segment
            let s = my_range.start.min(other_range.start);
            let e = my_range.start.max(other_range.start);
    
            if s == other_range.start && s != my_range.start {
                result.push(s..e);
            }
    
            // Middle segment
            let s = my_range.start.max(other_range.start);
            let e = my_range.end.min(other_range.end);
    
            let shift = self.lookup(s as usize) as isize - s;
            result.push((s + shift)..(e + shift));
    
            // Right segment
            let s = my_range.end.min(other_range.end);
            let e = my_range.end.max(other_range.end);
    
            if e == other_range.end && e != my_range.end {
                result.push(s..e);
            }
    
            result.into_iter().filter(|r| !r.is_empty()).collect()
        }
    }
    
    fn lookup_seed_location(almanac: &Almanac, seed: usize) -> usize {
        let mut location_old = seed;
        let mut location = location_old;
    
        for entries in almanac.maps.iter() {
            for range_map in entries.iter() {
                location = range_map.lookup(location);
                if location != location_old {
                    location_old = location;
                    break;
                }
    
                location_old = location;
            }
        }
    
        location
    }
    
    fn lookup_best_seed(almanac: &Almanac, start: usize, length: usize) -> usize {
        let mut q = VecDeque::new();
        let mut r = Vec::new();
    
        q.push_front(((start as isize)..(start as isize + length as isize), 0, 0));
    
        while let Some((rng, map_id, entry_id)) = q.pop_front() {
            if map_id >= almanac.maps.len() {
                r.push(rng.start as usize);
                continue;
            }
    
            let map = &almanac.maps[map_id];
    
            if entry_id >= map.len() {
                q.push_back((rng.clone(), map_id + 1, 0));
                continue;
            }
    
            let entry = &map[entry_id];
    
            let new_rngs = entry.range_split(rng.clone());
    
            for new_rng in new_rngs.iter() {
                if rng.clone().start == new_rng.start && rng.clone().end == new_rng.end {
                    q.push_back((rng.clone(), map_id, entry_id + 1));
                } else {
                    q.push_back((new_rng.clone(), map_id + 1, 0));
                }
            }
        }
    
        r.into_iter().min().unwrap_or(start)
    }
    
    fn part1(input: &str) -> usize {
        let almanac = parser::parse(input).unwrap().1;
    
        almanac
            .seeds
            .iter()
            .cloned()
            .map(|seed| lookup_seed_location(&almanac, seed))
            .min()
            .unwrap()
    }
    
    fn part2(input: &str) -> usize {
        let almanac = parser::parse(input).unwrap().1;
    
        let mut minimum = usize::MAX;
    
        for [start, length] in almanac.seeds.array_chunks() {
            let cur_best = lookup_best_seed(&almanac, *start, *length);
    
            if cur_best < minimum {
                minimum = cur_best;
            }
        }
    
        minimum
    }
    

    The code is a bit messy, but I'm too exhausted to clean it up right now. Time to go to bed.

    3 votes
    1. [2]
      first-must-burn
      Link Parent
      Just out of curiousity, have you looked at why the initial solution didn't solve the input? I'm assuming it was some kind of bug not triggered by the test case, but introspecting a little might...

      Just out of curiousity, have you looked at why the initial solution didn't solve the input? I'm assuming it was some kind of bug not triggered by the test case, but introspecting a little might help you improve your process.

      FWIW if something is complicated, I will write unit tests on as many components as can to avoid bugs. Probably I write too many (for speed), but it usually works on the first try in the end.

      1 vote
      1. DataWraith
        Link Parent
        Perhaps. I did have quite a few unit tests based on the test input or my understanding of the problem, for example, to make sure that the ranges are split properly, etc. But as I said, they were...

        Perhaps. I did have quite a few unit tests based on the test input or my understanding of the problem, for example, to make sure that the ranges are split properly, etc. But as I said, they were green.

        Potential spoiler

        My best guess is that there was a bug related to not properly skipping maps that were already used in the conversion in some instances. E.g. mapping to soil, and then mistakenly using a different row of the soil map again instead of going to the next one. In the end I got desperate and deleted everything, wrote everything from scratch again, and that worked.

        The biggest problem with my process was that I felt immense time pressure where there should have been none -- I wasn't even intentionally trying to be fast, because there's little point when you start many hours after the puzzle opens, but somehow I felt under pressure the entire time, which meant that a lot of the good practices I normally have (e.g. somewhat frequent commits) went right out of the window. I also felt very close to a solution for a long time and resorted to tweaking things here and there, hoping they would make it work, which is of course awful and should almost never be done...

        This problem was doubly-frustrating, because the algorithm itself was immediately obvious to me, but I got hung up on implementation bugs for literally hours...

        2 votes
  3. [3]
    Boojum
    Link
    It's fun to see this here. I've been posting visualizations each day over on /r/adventofcode. If you're stuck on Part 2, today's animation may help. Here's my Python solution. It takes about 0.06s...

    It's fun to see this here. I've been posting visualizations each day over on /r/adventofcode. If you're stuck on Part 2, today's animation may help.

    Here's my Python solution. It takes about 0.06s for Part 2 on my ancient 2009-era desktop. Initially I'd explicitly handled each of the different possible ways the ranges could overlap. But after sleeping on it, I realized that if I represent the ranges as half-open intervals then I could just sort the four end points and pair-wise shingle them to get subranges that cover the parent ranges without cross the bounds. That got my solution down to 27 lines here.

    Part 1 and 2
    import sys
    
    bks = [ bks.splitlines() for bks in sys.stdin.read().split( "\n\n" ) ]
    sds = [ int( i ) for i in bks[ 0 ][ 0 ].split()[ 1 : ] ]
    mps = [ [ [ int( i ) for i in ln.split() ]
              for ln in bk[ 1 : ] ]
            for bk in bks[ 1 : ] ]
    
    
    # vls = [ ( b, b + 1 ) for b in sds ]                                   # For Part 1
    vls = [ ( b, b + l ) for b, l in zip( sds[ : : 2 ], sds[ 1 : : 2 ] ) ]  # For Part 2
    for mp in mps:
        nxtm = []
        for rd, rs, rl in mp:
            nxtr = []
            for vl, vh in vls:
                s = sorted( [ vl, vh, rs, rs + rl ] )
                for l, h in zip( s, s[ 1 : ] ):
                    if vl <= l < h <= vh:
                        if rs <= l < h <= rs + rl:
                            nxtm.append( ( l - rs + rd, h - rs + rd ) )
                        else:
                            nxtr.append( ( l, h ) )
            vls = nxtr
        vls += nxtm
    
    print( min( l for l, h in vls ) )
    
    3 votes
    1. first-must-burn
      Link Parent
      That visualization is really nice. I especially like the way the animation progressively discloses the operation.

      That visualization is really nice. I especially like the way the animation progressively discloses the operation.

      2 votes
    2. Eabryt
      Link Parent
      Not gonna lie, that Visualization doesn't help me at all. Not sure if that's because it's going too fast or just because I'm struggling with this one overall.

      Not gonna lie, that Visualization doesn't help me at all. Not sure if that's because it's going too fast or just because I'm struggling with this one overall.

      1 vote
  4. scarecrw
    (edited )
    Link
    Full Solution in Haskell module Main (main) where import AOCTools ( splitBy ) main :: IO () main = do input <- readFile "./input.txt" putStrLn $ "Part 1: " ++ show (solve1 $ parseInput input)...
    Full Solution in Haskell
    module Main (main) where
    
    import AOCTools ( splitBy )
    
    main :: IO ()
    main = do
        input <- readFile "./input.txt"
        putStrLn $ "Part 1: " ++ show (solve1 $ parseInput input)
        putStrLn $ "Part 2: " ++ show (solve2 $ parseInput input)
    
    -- Data Types & Parsing
    
    data ProductionMapping = Mapping {
        sourceName :: String,
        destinationName :: String,
        mappings :: [RangeMap]
    } deriving (Show)
    
    data RangeMap = RangeMap {
        destinationStart :: Integer,
        sourceStart :: Integer,
        rangeLength :: Integer
    } deriving (Show)
    
    data Range = Range {
        start :: Integer,
        end :: Integer
    } deriving (Show)
    
    parseInput :: String -> ([Integer], [ProductionMapping])
    parseInput input = (seeds, prodMaps) where
        sections = splitBy "\n\n" input
        seeds = map read (tail . words . head $ sections)
        prodMaps = map parseProductionMapping $ tail sections
    
    parseProductionMapping :: String -> ProductionMapping
    parseProductionMapping input = Mapping source dest maps where
        [source, _, dest] = take 3 (splitBy "-" (head (words input)))
        maps = map parseRangeMap $ tail (lines input)
    
    parseRangeMap :: String -> RangeMap
    parseRangeMap s = RangeMap dst src rng where
        [dst, src, rng] = map read $ words s
    
    -- Helper Functions
    
    rangeIntersection:: Range -> Range -> Maybe Range
    rangeIntersection(Range a b) (Range c d)
        | b <= c = Nothing
        | a >= d = Nothing
        | otherwise = Just $ Range (max a c) (min b d)
    
    rangeSubtraction :: Range -> Range -> [Range]
    rangeSubtraction (Range a b) (Range c d) = filter (\r -> start r < end r) ranges where
        ranges = [Range a (min b c), Range (max a d) b]
    
    -- Part 1
    
    solve1 :: ([Integer], [ProductionMapping]) -> Integer
    solve1 (seeds, prodMaps) = minimum locations where
        locations = map (findLocation "seed") seeds
        findLocation :: String -> Integer -> Integer
        findLocation "location" val = val
        findLocation stage val = uncurry findLocation $ findNextStage prodMaps stage val
    
    findNextStage :: [ProductionMapping] -> String -> Integer -> (String, Integer)
    findNextStage prodMaps stage val = (stage', val') where
        mapping = head $ filter (\m -> sourceName m == stage) prodMaps
        stage' = destinationName mapping
        val' = processStage (mappings mapping) val
    
    processStage :: [RangeMap] -> Integer -> Integer
    processStage [] v = v
    processStage ((RangeMap dst src rng):remMaps) v
        | src <= v && v < src + rng = v - src + dst
        | otherwise = processStage remMaps v
    
    -- Part 2
    
    solve2 :: ([Integer], [ProductionMapping]) -> Integer
    solve2 (seeds, prodMaps) = minimum (map start locations) where
        seedRanges = pairUp seeds where
            pairUp [] = []
            pairUp (a:b:t) = Range a (a+b) : pairUp t
        locations = findLocation "seed" seedRanges
        findLocation :: String -> [Range] -> [Range]
        findLocation "location" ranges = ranges
        findLocation stage ranges = uncurry findLocation $ findNextStageRange prodMaps stage ranges
    
    findNextStageRange :: [ProductionMapping] -> String -> [Range] -> (String, [Range])
    findNextStageRange prodMaps stage ranges = (stage', ranges') where
        mapping = head $ filter (\m -> sourceName m == stage) prodMaps
        stage' = destinationName mapping
        ranges' = concatMap (processStageRange (mappings mapping)) ranges
    
    processStageRange :: [RangeMap] -> Range -> [Range]
    processStageRange [] r = [r]
    processStageRange ((RangeMap dst src rng):remMaps) r = case rangeIntersection r srcRange of
        Nothing -> processStageRange remMaps r
        Just (Range a b) -> overlap : remainder where
            overlap = Range (a - src + dst) (b - src + dst)
            remainder = concatMap (processStageRange remMaps) (rangeSubtraction r srcRange)
        where
            srcRange = Range src (src + rng)
    

    Looking back over my code, things look a bit overcomplicated, though I'm not sure exactly what I'd change. I only began using a Range type in part 2, and probably would have benefited from using that from the beginning, making each mapping consist of a source range and a displacement.

    One thing I am happy with is my range utilities:

    Range Utilities
    data Range = Range {
        start :: Integer,
        end :: Integer
    } deriving (Show)
    
    rangeIntersection :: Range -> Range -> Maybe Range
    rangeIntersection (Range a b) (Range c d)
        | b <= c = Nothing
        | a >= d = Nothing
        | otherwise = Just $ Range (max a c) (min b d)
    
    rangeSubtraction :: Range -> Range -> [Range]
    rangeSubtraction (Range a b) (Range c d) = filter (\r -> start r < end r) ranges where
        ranges = [Range a (min b c), Range (max a d) b]
    
    Nothing complicated there, but I know I *could* have made this a mess of conditional statements and managed to avoid that.

    Still enjoying exploring Haskell! I'm only about 1/3 of the way through the book I've been following as a guide. I doubt I'll finish by the end of AoC, but it already has my recommendation for getting me this far.

    2 votes
  5. [6]
    Eabryt
    Link
    This is killing me mostly just because I am having a hard time understanding the actual problem, I sometimes hate how little example they give. I'm hoping someone can help. In the example they of...

    This is killing me mostly just because I am having a hard time understanding the actual problem, I sometimes hate how little example they give. I'm hoping someone can help.

    In the example they of course run through the seed-to-soil map. For seed 79, it says soil number is 81.

    What I'm a bit confused about is when we get to the soil-to-fertilizer map. We don't touch seed 79, so why is the fertilizer number 81 instead of 79? When I'm going through that first map should I basically be "initializing" all the later values (fertilizer, water, light, etc) to that initial soil number? The wording is a bit unclear sometimes and I really wish they would have an idiot like me help with with description/examples.

    2 votes
    1. [5]
      guissmo
      Link Parent
      You can think of them as functions. The two seed-to-soil maps: 50 98 2 52 50 48 Are the maps f: 98...99 |-> 50...51 g: 50...97 |-> 52...99 f takes seed 98 and makes it soil 50 f takes seed 99 and...

      You can think of them as functions.

      The two seed-to-soil maps:

      50 98 2
      52 50 48
      

      Are the maps

      f: 98...99 |-> 50...51
      g: 50...97 |-> 52...99
      

      f takes seed 98 and makes it soil 50
      f takes seed 99 and makes it soil 51

      g takes seed 50 and makes it soil 52
      g takes seed 51 and makes it soil 53
      ...
      g takes seed 79 and makes it soil 81
      ...
      g takes seed 97 and makes it soil 99

      For all seeds that are not "transformed" be these maps, their number stays the same.

      For example, none of the seed-to-soil maps transforms seed 42.
      So seed 42 transforms to soil 42.

      2 votes
      1. [4]
        Eabryt
        (edited )
        Link Parent
        Yeah I understand all that. The problem comes when we go to the next map. Sticking with seed 42. If I understand correctly, the first line of 0 15 37 would mean that for Seed 42 it would have Soil...

        Yeah I understand all that.

        The problem comes when we go to the next map.

        Sticking with seed 42.

        If I understand correctly, the first line of 0 15 37 would mean that for Seed 42 it would have Soil 42, and now Fertilizer value of 27.

        However, now we've got seed 99. It has a soil of 51, which isn't in that range, so does that mean fertilizer should be set to 51, or 99?

        edit: Also, For that 0 15 37, that's saying when soil is 15, it matches to fertilizer 0 right?

        1 vote
        1. [3]
          guissmo
          Link Parent
          I see what you mean and I guess this was the relevant but confusing statement. The source category for moving from soil to fertilizer is soil and the destination category is then fertilizer. So...

          I see what you mean and I guess this was the relevant but confusing statement.

          Any source numbers that aren't mapped correspond to the same destination number. So, seed number 10 corresponds to soil number 10.

          The source category for moving from soil to fertilizer is soil and the destination category is then fertilizer. So with your question:

          seed (99) -> soil(51) -> fertilizer(51)
          

          edit: Also, For that 0 15 37, that's saying when soil is 15, it matches to fertilizer 0 right?

          Yes.

          Anyway, it was using the source/destination category terms a few paragraphs up but I could understand how one could miss that given how verbose the problem was.

          The rest of the almanac contains a list of maps which describe how to convert numbers from a source category into numbers in a destination category. That is, the section that starts with seed-to-soil map: describes how to convert a seed number (the source) to a soil number (the destination). This lets the gardener and his team know which soil to use with which seeds, which water to use with which fertilizer, and so on.

          Rather than list every source number and its corresponding destination number one by one, the maps describe entire ranges of numbers that can be converted. Each line within a map contains three numbers: the destination range start, the source range start, and the range length.

          PS: Is it bad for Tildes that I'm quoting parts of the problem here?

          2 votes
          1. [2]
            Eabryt
            Link Parent
            Okay cool. Final (hopefully) question. Going to spoiler it just to be safe. Spoilers Am I correct that once we get past the first map, it's possible that the value it's referencing might have more...

            Okay cool. Final (hopefully) question. Going to spoiler it just to be safe.

            Spoilers

            Am I correct that once we get past the first map, it's possible that the value it's referencing might have more than one matching map?

            AKA. Looking at 39 0 15 for the soil-to-fertilizer map. When we're doing our range we get to a soil value of 14 -> 39

            Currently we have soil 14 showing up twice in our list. Once for Seed 14 (it didn't have a soil set in the first map), and once for Seed 29 (which was set in the previous map), should both seed fertilizer values be set to 39?

            1 vote
            1. guissmo
              Link Parent
              I didn’t think about this constraint while solving it but I think it would be allowed. Similarly, some fertilizers would not be accessible. Like fertilizer 0 will not be possible to obtain.

              I didn’t think about this constraint while solving it but I think it would be allowed.

              Similarly, some fertilizers would not be accessible. Like fertilizer 0 will not be possible to obtain.

              1 vote
  6. infinitesimal
    Link
    I initially used my part 1 solution to write a naive part 2 solution, which worked fine on the example but failed on the input for obvious reasons. After trying (and failing) to write a proper...

    I initially used my part 1 solution to write a naive part 2 solution, which worked fine on the example but failed on the input for obvious reasons. After trying (and failing) to write a proper solution using intervals and ending up with a bazillion intervals all looking wrong, I slept on it before getting a working solution: change the representation of intervals to (start, end, shift) and augment my intervals with sentinels (with a shift of 0) so that the entire 0 to Long.MAX_VALUE space is partitioned by intervals. The former simplified the arithmetic and the latter reduced 6 cases to 2, and then everything worked. 🎉

    Kotlin
    package aoc2023
    
    object Day5 {
        data class Range(val start: Long, val end: Long, val shift: Long)
    
        fun parse(input: String): Pair<List<Long>, List<List<Range>>> {
            fun longsOf(s: String) = Regex("""\d+""").findAll(s).map { it.value.toLong() }
            fun rangeOf(s: String) = longsOf(s).toList().let { (dest, src, n) -> Range(src, src + n, dest - src) }
            fun augment(map: List<Range>): List<Range> {
                val sentinels = mutableListOf<Range>()
                if (map.first().start != 0L) {
                    sentinels.add(Range(0, map.first().start, 0))
                }
                for (i in map.indices.drop(1)) {
                    if (map[i - 1].end != map[i].start) {
                        sentinels.add(Range(map[i - 1].end, map[i].start, 0))
                    }
                }
                if (map.last().end != Long.MAX_VALUE) {
                    sentinels.add(Range(map.last().end, Long.MAX_VALUE, 0))
                }
                return (map + sentinels).sortedBy { it.start }
            }
    
            fun mapOf(s: String) = s.lines().drop(1).map { rangeOf(it) }.sortedBy { it.start }.let { augment(it) }
            val parts = input.trim().split("\n\n")
            val seeds = longsOf(parts.first()).toList()
            val maps = parts.drop(1).map { mapOf(it) }
            return Pair(seeds, maps)
        }
    
        fun part1(input: String): Long {
            val (seeds, maps) = parse(input)
            fun convert(seed: Long, maps: List<List<Range>>) = maps.fold(seed) { num, map ->
                val range = map.first { it.start <= num && num < it.end }
                num + range.shift
            }
            return seeds.minOf { convert(it, maps) }
        }
    
        fun part2(input: String): Long {
            val (seeds, maps) = parse(input)
            val chunks0 = seeds.chunked(2).map { (start, n) -> Range(start, start + n, 0) }
            fun convert1(chunk0: Range, map: List<Range>): List<Range> {
                val inChunks = ArrayDeque<Range>()
                inChunks.addLast(chunk0)
                val outChunks = ArrayDeque<Range>()
                while (inChunks.isNotEmpty()) {
                    val chunk = inChunks.removeFirst()
                    val range = map.first { it.start <= chunk.start && chunk.start < it.end }
                    if (chunk.end <= range.end) {
                        outChunks.addLast(Range(chunk.start + range.shift, chunk.end + range.shift, 0))
                    } else {
                        outChunks.addLast(Range(chunk.start + range.shift, range.end + range.shift, 0))
                        inChunks.addLast(Range(range.end, chunk.end, 0))
                    }
                }
                return outChunks
            }
            return maps.fold(chunks0) { chunks, map -> chunks.flatMap { chunk -> convert1(chunk, map) } }.minOf { it.start }
        }
    }
    
    2 votes
  7. RheingoldRiver
    Link
    I was chatting with a friend about part 2, he brute-forced it and I drew a picture explaining how to not brute-force it, here's the picture Part 1 import json from copy import copy class Solver:...

    I was chatting with a friend about part 2, he brute-forced it and I drew a picture explaining how to not brute-force it, here's the picture

    Part 1
    import json
    from copy import copy
    
    
    class Solver:
    
        def __init__(self):
            with open('info.json', 'r', encoding='utf-8') as f:
                self.raw_data = json.load(f)
            self.data = self.parse_lines()
    
        def parse_lines(self):
            data = {
                'seeds': [int(x) for x in self.raw_data['seeds'].split()],
                'maps': [self.parse_map(x) for x in self.raw_data['maps']]
            }
            return data
    
        @classmethod
        def parse_map(cls, map_set):
            return [cls.parse_one_entry(x) for x in map_set.split('\n')]
    
        @staticmethod
        def parse_one_entry(entry):
            destination = int(entry.split()[0])
            source = int(entry.split()[1])
            length = int(entry.split()[2])
            return {
                'destination': destination,
                'source': source,
                'length': length,
                'difference': destination - source,
                'min': source,
                'max': source + length,
            }
    
        def run(self):
            seeds = copy(self.data['seeds'])
            for seed_map in self.data['maps']:
                seeds = [self.do_map(seed, seed_map) for seed in seeds]
            return min(seeds)
    
        @staticmethod
        def do_map(n, seed_map):
            for map_item in seed_map:
                if map_item['max'] > n >= map_item['min']:
                    return n + map_item['difference']
            return n
    
    
    if __name__ == '__main__':
        print(Solver().run())
    
    Part 2
    import json
    from copy import deepcopy
    
    
    class Solver:
    
        def __init__(self):
            with open('info.json', 'r', encoding='utf-8') as f:
                self.raw_data = json.load(f)
            self.data = self.parse_lines()
    
        def parse_lines(self):
            data = {
                'seeds': [self.parse_seed(x) for x in self.raw_data['seeds'].split(';')],
                'maps': [self.parse_map(x) for x in self.raw_data['maps']]
            }
            return data
    
        @staticmethod
        def parse_seed(seed):
            start = int(seed.split()[0])
            length = int(seed.split()[1])
            return {
                'min': start,
                'max': start + length - 1
            }
    
        @classmethod
        def parse_map(cls, map_set):
            return [cls.parse_one_entry(x) for x in map_set.split('\n')]
    
        @staticmethod
        def parse_one_entry(entry):
            destination = int(entry.split()[0])
            source = int(entry.split()[1])
            length = int(entry.split()[2])
            return {
                'difference': destination - source,
                'min': source,
                'max': source + length - 1,
            }
    
        def run(self):
            seeds = deepcopy(self.data['seeds'])
            for seed_map in self.data['maps']:
                found_this_round = []
                not_found_yet = []
                for map_item in seed_map:
                    not_found_yet = []
                    print(f'about to process: {str(seeds)}')
                    for seed in seeds:
                        if map_item['max'] < seed['min']:
                            print(f'case 1 - {str(seed)}')
                            not_found_yet.append(seed)
                            continue
                        if map_item['min'] > seed['max']:
                            print(f'case 2 - {str(seed)}')
                            not_found_yet.append(seed)
                            continue
                        if map_item['min'] <= seed['min'] and map_item['max'] >= seed['max']:
                            # map contains the entire seed round; we dont need to look anymore
                            print(f'case 3 - {str(seed)}')
                            found_this_round.append({
                                'min': seed['min'] + map_item['difference'],
                                'max': seed['max'] + map_item['difference'],
                            })
                            continue
                        if seed['min'] < map_item['min'] <= seed['max'] <= map_item['max']:
                            # the map item min is between seed min and seed max
                            print(f'case 4 - {str(seed)}')
                            # what we didnt find
                            not_found_yet.append({
                                'min': seed['min'],
                                'max': map_item['min'] - 1
                            })
                            # what we found
                            found_this_round.append({
                                'min': map_item['min'] + map_item['difference'],
                                'max': seed['max'] + map_item['difference']
                            })
                        if map_item['min'] <= seed['min'] <= map_item['max'] < seed['max']:
                            print(f'case 5 - {str(seed)}')
                            found_this_round.append({
                                'min': seed['min'] + map_item['difference'],
                                'max': map_item['max'] + map_item['difference']
                            })
                            not_found_yet.append({
                                'min': map_item['max'] + 1,
                                'max': seed['max']
                            })
                        if map_item['min'] > seed['min'] and map_item['max'] < seed['max']:
                            print(f'case 6 - {str(seed)}')
                            found_this_round.append({
                                'min': map_item['min'] + map_item['difference'],
                                'max': map_item['max'] + map_item['difference'],
                            })
                            not_found_yet.append({
                                'min': seed['min'],
                                'max': map_item['min'] - 1,
                            })
                            not_found_yet.append({
                                'min': map_item['max'] + 1,
                                'max': seed['max'],
                            })
                    seeds = not_found_yet
                    print(f"one map range: {str(seeds)} ({map_item['min']} -> {map_item['max']})")
                seeds = found_this_round + not_found_yet
                print(f"new entire round**************: {str(seeds)}")
    
            return min([seed['min'] for seed in seeds])
    
        @staticmethod
        def do_map(n, seed_map):
            for map_item in seed_map:
                if map_item['max'] > n >= map_item['min']:
                    return n + map_item['difference']
            return n
    
    
    if __name__ == '__main__':
        print(Solver().run())
    
    1 vote
  8. whs
    (edited )
    Link
    Today's language of the day is my very first language, Visual Basic for Application. With GUI, no less! I can't remember when was the last time I wrote VBA, but surely I didn't know how to use...

    Today's language of the day is my very first language, Visual Basic for Application. With GUI, no less! I can't remember when was the last time I wrote VBA, but surely I didn't know how to use dynamic sized array.

    Part 1
    Dim Seeds() As LongLong
    Dim SeedSoilMap() As New RangeMap
    Dim SoilToFertilizerMap() As New RangeMap
    Dim FertilizerToWaterMap() As New RangeMap
    Dim WaterToLightMap() As New RangeMap
    Dim LightToTemperatureMap() As New RangeMap
    Dim TemperatureToHumidityMap() As New RangeMap
    Dim HumidityToLocationMap() As New RangeMap
    
    Function ParseInput(Problem As String)
        Lines = Split(Problem, vbCr & vbLf)
        
        'Seeds
        SeedsRaw = Split(Lines(0), " ")
        SeedCount = GetLength(SeedsRaw) - 1
        ReDim Seeds(0 To SeedCount - 1)
        For i = 0 To SeedCount - 1
            Casted = CLngLng(SeedsRaw(i + 1))
            Seeds(i) = Casted
        Next i
        
        ParseMap Lines, "seed-to-soil", SeedSoilMap
        ParseMap Lines, "soil-to-fertilizer", SoilToFertilizerMap
        ParseMap Lines, "fertilizer-to-water", FertilizerToWaterMap
        ParseMap Lines, "water-to-light", WaterToLightMap
        ParseMap Lines, "light-to-temperature", LightToTemperatureMap
        ParseMap Lines, "temperature-to-humidity", TemperatureToHumidityMap
        ParseMap Lines, "humidity-to-location", HumidityToLocationMap
    End Function
    
    Sub ParseMap(Lines As Variant, Name As String, ByRef Target As Variant)
        found = False
        Index = 0
        ReDim Target(0)
        
        For Each Line In Lines
            If found And Line = "" Then
                Exit For
            ElseIf found Then
                Cols = Split(Line, " ")
                ReDim Preserve Target(Index)
                Set Target(Index) = New RangeMap
                Target(Index).From CLngLng(Cols(0)), CLngLng(Cols(1)), CLngLng(Cols(2))
                Index = Index + 1
            ElseIf Line = Name & " map:" Then
                found = True
            End If
        Next Line
    End Sub
    
    
    Function GetLength(a As Variant) As Integer
        If IsEmpty(a) Then
            GetLength = 0
        Else
            GetLength = UBound(a) - LBound(a) + 1
        End If
    End Function
    
    Function GetMappedRange(ByRef Ranges As Variant, ByVal Index As LongLong) As LongLong
        For Each Rng In Ranges
            If Rng.Src <= Index And Rng.SrcEnd >= Index Then
                Offset = Index - Rng.Src
                GetMappedRange = Rng.Dest + Offset
                Exit Function
            End If
        Next Rng
        'If unmapped, then use the same output
        GetMappedRange = Index
    End Function
    
    Function Solve1() As String
        Debug.Print "New Solve1"
        Out = 9.22337203685478E+18
        For Each Seed In Seeds
            Soil = GetMappedRange(SeedSoilMap, Seed)
            Fertilizer = GetMappedRange(SoilToFertilizerMap, Soil)
            Water = GetMappedRange(FertilizerToWaterMap, Fertilizer)
            Light = GetMappedRange(WaterToLightMap, Water)
            Temperature = GetMappedRange(LightToTemperatureMap, Light)
            Humidity = GetMappedRange(TemperatureToHumidityMap, Temperature)
            Location = GetMappedRange(HumidityToLocationMap, Humidity)
            
            Debug.Print Seed & " - " & Soil & " - " & Fertilizer & " - " & Water & " - " & Light & " - " & Temperature & " - " & Humidity & " - " & Location
            If Location < Out Then
                Out = Location
            End If
        Next Seed
        Solve1 = Out
    End Function
    
    Function Solve2() As String
        Debug.Print "New Solve2"
        Out = 9.22337203685478E+18
        For i = 0 To GetLength(Seeds) - 1 Step 2
            RangeStart = Seeds(i)
            RangeLen = Seeds(i + 1)
            For Seed = RangeStart To RangeStart + RangeLen
                Soil = GetMappedRange(SeedSoilMap, Seed)
                Fertilizer = GetMappedRange(SoilToFertilizerMap, Soil)
                Water = GetMappedRange(FertilizerToWaterMap, Fertilizer)
                Light = GetMappedRange(WaterToLightMap, Water)
                Temperature = GetMappedRange(LightToTemperatureMap, Light)
                Humidity = GetMappedRange(TemperatureToHumidityMap, Temperature)
                Location = GetMappedRange(HumidityToLocationMap, Humidity)
                
                Debug.Print Seed & " - " & Soil & " - " & Fertilizer & " - " & Water & " - " & Light & " - " & Temperature & " - " & Humidity & " - " & Location
                If Location < Out Then
                    Out = Location
                End If
            Next Seed
        Next i
        Solve2 = Out
    End Function
    
    Private Sub CommandButton1_Click()
        ParseInput (InputBox.Value)
        OutputBox.Value = Solve1()
    End Sub
    
    Private Sub CommandButton2_Click()
        ParseInput (InputBox.Value)
        OutputBox.Value = Solve2()
    End Sub
    

    RangeMap.cls

    '[Dest, Dest + Len)
    Public Dest As LongLong
    '[Src, Src + Len)
    Public Src As LongLong
    Public Length As LongLong
    
    Public Sub From(d As LongLong, s As LongLong, l As LongLong)
        Dest = d
        Src = s
        Length = l
    End Sub
    
    
    'Index of the last src member
    Public Property Get DestEnd() As LongLong
        DestEnd = Dest + Length - 1
    End Property
    
    'Index of the last src member
    Public Property Get SrcEnd() As LongLong
        SrcEnd = Src + Length - 1
    End Property
    

    It is so slow that I brute forced part 2 while went out to the doctor, and it haven't finished yet. I thought Excel is good at data processing.

    For part 2 I suppose if I want to brute force, doing it in Rust would make it really fast. Add Rayon on top and I can put my i9 to work.

    Part 2
    use std::fmt::{Debug, Formatter};
    use std::io;
    use std::io::Read;
    use rayon::prelude::*;
    
    /// Map from a src-dst to another range of the same size
    struct RangeMap {
        pub src: u64,
        pub dst: u64,
        pub len: u64,
    }
    
    impl RangeMap {
        pub fn src_end(&self) -> u64 {
            self.src + self.len - 1
        }
        pub fn dst_end(&self) -> u64 {
            self.dst + self.len - 1
        }
    }
    
    impl Debug for RangeMap {
        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
            write!(f, "{}..={} -> {}..={}", self.src, self.src_end(), self.dst, self.dst_end())
        }
    }
    
    #[derive(Debug)]
    struct Problem {
        pub seeds: Vec<u64>,
        pub maps: Vec<Vec<RangeMap>>,
    }
    
    fn parse_input(input: &str) -> Problem {
        let mut lines = input.lines();
        // parse seed
        let seed_line = lines.next().unwrap();
        let seeds = seed_line["seeds: ".len()..].split(" ").map(|i| i.parse().unwrap()).collect();
    
        let _ = lines.next().unwrap();
        // parse the maps
        let mut maps = Vec::new();
        let mut cur_map = None;
        for line in lines {
            if line == "" {
                continue
            } else if line.ends_with(" map:") {
                maps.push(Vec::new());
                cur_map = maps.last_mut();
            } else {
                let mut numbers = line.split(" ").map(|v| v.parse().unwrap());
                cur_map.as_mut().unwrap().push(RangeMap {
                    dst: numbers.next().unwrap(),
                    src: numbers.next().unwrap(),
                    len: numbers.next().unwrap(),
                })
            }
        }
    
        Problem {
            seeds,
            maps,
        }
    }
    
    fn get_mapped(ranges: &Vec<RangeMap>, input: u64) -> u64 {
        for range in ranges {
            if range.src <= input && range.src_end() >= input {
                let offset = input - range.src;
                return range.dst + offset;
            }
        }
        // If unmatched, then return input
        input
    }
    
    fn walk_ranges(ranges: &Vec<Vec<RangeMap>>, input: u64) -> u64 {
        let mut out = input;
        for range in ranges {
            out = get_mapped(range, out);
        }
        return out;
    }
    
    fn main() {
        let mut stdin = io::stdin();
        let mut input = String::new();
        stdin.read_to_string(&mut input).unwrap();
    
        let problem = parse_input(&input);
        println!("{:?}", problem);
    
        // let problem1 = problem.seeds.iter().map(|v| walk_ranges(&problem.maps, *v)).min().unwrap();
        // println!("Problem1: {}", problem1);
    
        let ranges: Vec<_> = problem.seeds.chunks(2)
            .map(|v| v[0]..(v[0]+v[1]))
            .collect();
    
        println!("total pairs: {}", ranges.len());
    
        let problem2 = ranges.into_par_iter()
            .map(|rng| {
                rng.into_iter().map(|v| walk_ranges(&problem.maps, v)).min().unwrap()
            })
            .inspect(|min_val| {
                println!("Finished a range -> {}", min_val);
            })
            .min().unwrap();
        println!("Problem2: {}", problem2);
    }
    

    This takes 25s of wallclock time and 91s CPU time in total. I got another idea on how to brute force it, maybe I'll look into VBA again...

    edit: I got another solution in VBA brute force. That only took about 4 hours and a half at 3,200 iterations per second.

    1 vote
  9. jonah
    Link
    Today, my choice of using TypeScript is biting me. Part 1 was not very difficult. I made the naive mistake of using actual hashmaps in my first implementation, and then I saw the actual puzzle...

    Today, my choice of using TypeScript is biting me.

    Part 1 was not very difficult. I made the naive mistake of using actual hashmaps in my first implementation, and then I saw the actual puzzle input, and realized I needed to rework it.

    Part 1
    import { getInput } from "./input";
    import * as utils from "./utils";
    
    interface Record {
      dst: number;
      src: number;
      len: number;
    }
    
    (async () => {
      const input = getInput();
      const lines = input.split("\n");
    
      const seeds = utils.ints(lines[0]);
      const records: Record[][] = [];
    
      let currRecords: Record[] = [];
      for (let i = 2; i < lines.length; i++) {
        const line = lines[i];
        if (!line) {
          records.push(currRecords);
          continue;
        }
    
        if (line.includes("map")) {
          currRecords = [];
        } else {
          const nums = utils.ints(line);
          currRecords!.push({ dst: nums[0], src: nums[1], len: nums[2] });
        }
      }
    
      let lowest = Number.MAX_VALUE;
      seeds.forEach((seed) => {
        let num = seed;
        for (let i = 0; i < records.length; i++) {
          const record = records[i].find(
            (r) => r.src <= num && r.src + r.len > num,
          );
          if (record) {
            num = record.dst + (num - record.src);
          }
        }
        if (num < lowest) {
          lowest = num;
        }
      });
    
      console.log(lowest);
    })();
    

    Part 2 was not a very big change for me, because like many others, I just brute forced this one. I would like to figure out a more efficient solution, but I have to start work soon, so I will leave that for another time (which is probably never)

    Part 2
    import { getInput } from "./input";
    import * as utils from "./utils";
    
    interface Record {
      dst: number;
      src: number;
      len: number;
    }
    
    (async () => {
      const input = getInput();
      const lines = input.split("\n");
    
      const seeds = utils.ints(lines[0]);
      const records: Record[][] = [];
    
      let currRecords: Record[] = [];
      for (let i = 2; i < lines.length; i++) {
        const line = lines[i];
        if (!line) {
          records.push(currRecords);
          continue;
        }
    
        if (line.includes("map")) {
          currRecords = [];
        } else {
          const nums = utils.ints(line);
          currRecords!.push({ dst: nums[0], src: nums[1], len: nums[2] });
        }
      }
    
      let lowest = Number.MAX_VALUE;
      for (let n = 0; n < seeds.length; n += 2) {
        const seedStart = seeds[n];
        const seedLength = seeds[n + 1];
    
        for (let k = seedStart; k < seedStart + seedLength; k++) {
          let num = k;
          for (let i = 0; i < records.length; i++) {
            const record = records[i].find(
              (r) => r.src <= num && r.src + r.len > num,
            );
            if (record) {
              num = record.dst + (num - record.src);
            }
          }
          if (num < lowest) {
            lowest = num;
          }
        }
      }
    
      console.log(lowest);
    })();
    

    Had I used C++ or Rust, I don't think it would've taken as long to brute force. My machine clocked in 285s of runtime for this method. Oh well. Another day, another two stars.

    1 vote
  10. asciipip
    Link
    My solution in Common Lisp. One of the reasons I like Common Lisp is that it easily facilitates recursion, and one of the reason I like working recursively is that it lets you break problems apart...

    My solution in Common Lisp.

    One of the reasons I like Common Lisp is that it easily facilitates recursion, and one of the reason I like working recursively is that it lets you break problems apart into small pieces and work on the pieces individually. Here's the function that does the mapping seed (or whatever) ranges for part 2:

    map-range-on-range
    (defun map-range-on-range (ranges category-ranges &optional accumulated-ranges)
      (if (or (endp ranges)
              (endp category-ranges))
          (sort (append category-ranges accumulated-ranges) #'< :key #'first)
          (destructuring-bind (range-start range-end range-offset) (car ranges)
            (destructuring-bind (category-start category-end) (car category-ranges)
              (cond
                ((<= range-end category-start)
                 ;; No overlap with current range; go to the next one
                 (map-range-on-range (cdr ranges)
                                     category-ranges
                                     accumulated-ranges))
                ((<= category-end range-start)
                 ;; No overlap with current category; go to the next one
                 (map-range-on-range ranges
                                     (cdr category-ranges)
                                     (cons (car category-ranges) accumulated-ranges)))
                ((<= range-start category-start category-end range-end)
                 ;; Category fully within range; map and continue with next one
                 (map-range-on-range ranges
                                     (cdr category-ranges)
                                     (cons (list (+ category-start range-offset) (+ category-end range-offset))
                                           accumulated-ranges)))
                ((< category-start range-start)
                 ;; Some of category is before range; keep that bit and map the remainder
                 (map-range-on-range ranges
                                     (cons (list range-start category-end)
                                           (cdr category-ranges))
                                     (cons (list category-start range-start)
                                           accumulated-ranges)))
                ((< range-end category-end)
                 ;; Some of category is after range; map the in-range stuff and process the rest next time.
                 (map-range-on-range (cdr ranges)
                                     (cons (list range-end category-end)
                                           (cdr category-ranges))
                                     (cons (list (+ category-start range-offset) (+ range-end range-offset))
                                           accumulated-ranges)))
                (t
                 (assert nil nil "Missing test case")))))))
    

    In the function parameters, ranges is a list of three-element tuples, where the elements are the start and end of a “x-to-y map” range and the offset from the source to the destination numbers. (This is slightly different from the format presented in the problem, but I find this structure easier to work with.) category-ranges is a list of two-element tuples, giving the starts and ends of ranges of seed numbers (or other numbers mapped from seeds). For the example, the initial list of seed ranges is “(55, 68), (79, 93)”

    Notes: All ranges are half-open; they include the start number but exclude the end number. I keep ranges sorted by increasing start numbers for ease of processing. accumulated-ranges are the remapped ranges the function has gotten through already. Building that list as a function parameter (1) lets me easily sort it when I get to the end of the source data, and (2) lets the compiler employ tail-call elimination for all the recursive calls so it doesn't build up a long set of stack frames.

    So the function is mostly a big if-then statement that figures out which case applies, handles that specific case, and then calls itself recursively with new parameters to figure out and handle the next case.

    1 vote
  11. pnutzh4x0r
    Link
    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]...

    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 <= location < 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

    1 vote
  12. [4]
    first-must-burn
    Link
    I got the solution to part 1 under an hour last night, then banged out the brute force for part 2. It was going to take 8 minutes to finish without even trying to parallelize it, and I knew I was...

    I got the solution to part 1 under an hour last night, then banged out the brute force for part 2. It was going to take 8 minutes to finish without even trying to parallelize it, and I knew I was not going to develop the robust algorithm in 8 minutes, so I just let it run.

    I went back today and wrote general purpose Range class to do the set operations.
    There's an intersect operation that produces all the overlapping segments from two ranges. More properly, I should call it "Overlap" because it's not an intersect in the set notation sense. Each range carries user-defined metadata along with it, and (particularly useful), the Ranges output from the Intersect accumulate metadata from the ranges that overlap with it.

    The way I implemented it didn't quite work out, as I was thinking I could just remap the ranges and accumulate offsets from each layer, not thinking about the fact that I actually needed to do the transform at each layer. However, I was able to use the range operations and the metadata as markers to correctly implement the algorithm. So a little bit roundabout, but the end result is pleasingly fast.

    All in it took me a lot longer than I expected, but a lot of it was writing unit tests for the Range operations because I hope to reuse them.

    The solution is here

    One thing I'm noticing is that my code is very verbose compared to a lot of the solutions. In my mind, it's more maintainable or testable, but I'm not even sure that's true, or how to feel about it. Maybe it is a product of early training in Java and C++.

    1 vote
    1. [3]
      Toric
      Link Parent
      My rust solutions tend to also be a bit more verbose than other peeps it seems, I usually start by making data strucutres to parse the input into and then impl functions on those structures. I...

      My rust solutions tend to also be a bit more verbose than other peeps it seems, I usually start by making data strucutres to parse the input into and then impl functions on those structures. I also put actual unit tests in my code as well.

      2 votes
      1. [2]
        first-must-burn
        Link Parent
        In the real world, I want to write good code first, and how fast is a secondary consideration. I think rushing things (skipping review, unit tests, etc) makes you slower overall because debugging...

        In the real world, I want to write good code first, and how fast is a secondary consideration. I think rushing things (skipping review, unit tests, etc) makes you slower overall because debugging weird problems later is a lot more work. Sort of a tortoise and hare situation.

        I can see that placing high in AoC requires a different skill, something like "knowing how to do the minimum needed to solve the problem". I'm not sure that is something I want to learn even if placing in the top 100 (or 1000) would be cool.
        So maybe I should stop worrying and learn to love 4000th place.

        1 vote
        1. Toric
          Link Parent
          Yah, I dont even stay up late for them, just do them in the afternoon on my lunchbreak. If I can do it the day of, im proud of myself. (day 5 took me an extra day to figure out the range...

          Yah, I dont even stay up late for them, just do them in the afternoon on my lunchbreak. If I can do it the day of, im proud of myself. (day 5 took me an extra day to figure out the range splitting, including notebook scribbles.)

          2 votes
  13. wycy
    Link
    Rust It's not pretty. Brute forced for part 2, though it's at least parallelized so it takes ~16 seconds on an M2. Day 5 use std::env; use std::io::{self}; use rayon::prelude::*; extern crate...

    Rust

    It's not pretty. Brute forced for part 2, though it's at least parallelized so it takes ~16 seconds on an M2.

    Day 5
    use std::env;
    use std::io::{self};
    
    use rayon::prelude::*;
    
    extern crate regex;
    use regex::Regex;
    
    extern crate itertools;
    use itertools::Itertools;
    
    struct Mapping {
        dest_range_start: usize,
        source_range_start: usize,
        range_length: usize,
    }
    impl Mapping {
        pub fn in_source_range(&self, number: usize) -> bool {
            number >= self.source_range_start && number <= self.source_range_start + self.range_length - 1
        }
    }
    impl From<&str> for Mapping {
        fn from(s: &str) -> Self {
            let re = Regex::new(r"(\d+) (\d+) (\d+)").unwrap();
            let matches = re.captures(&s).unwrap();
            Self {
                dest_range_start:   matches[1].parse().unwrap(),
                source_range_start: matches[2].parse().unwrap(),
                range_length:       matches[3].parse().unwrap(),
            }
        }
    }
    
    struct Day5Map {
        mappings: Vec<Mapping>,
    }
    impl Day5Map {
        fn load_mappings(lines: &str) -> Self {
            let mappings = lines.split("\n").skip(1).map(Mapping::from).collect();
            Self { mappings: mappings }
        }
        pub fn destination(&self, from: usize) -> usize {
            for map in &self.mappings {
                if map.in_source_range(from) {
                    return map.dest_range_start + (from - map.source_range_start);
                }
            }
            from
        }
    }
    
    fn read_seeds(s: &str) -> Vec<usize> {
        let re = Regex::new(r"(\d+)").unwrap();
        let matches: Vec<_> = re
            .find_iter(s)
            .map(|x| x.as_str().parse::<usize>().unwrap())
            .collect();
        matches
    }
    
    fn solve(input: &str) -> io::Result<()> {
    
        // Input
        let input_str = std::fs::read_to_string(input).unwrap();
        let input_str = input_str.trim();
        let input: Vec<_> = input_str.split("\n\n").collect();
    
        // Seeds & mappings
        let seeds = read_seeds(input[0]);
        let seed_to_soil_map            = Day5Map::load_mappings(input[1]);
        let soil_to_fertilizer_map      = Day5Map::load_mappings(input[2]);
        let fertilizer_to_water_map     = Day5Map::load_mappings(input[3]);
        let water_to_light_map          = Day5Map::load_mappings(input[4]);
        let light_to_temperature_map    = Day5Map::load_mappings(input[5]);
        let temperature_to_humidity_map = Day5Map::load_mappings(input[6]);
        let humidity_to_location_map    = Day5Map::load_mappings(input[7]);
    
        // Part 1
        let part1 = seeds
            .iter()
            .map(|x| seed_to_soil_map           .destination(*x))
            .map(|x| soil_to_fertilizer_map     .destination( x))
            .map(|x| fertilizer_to_water_map    .destination( x))
            .map(|x| water_to_light_map         .destination( x))
            .map(|x| light_to_temperature_map   .destination( x))
            .map(|x| temperature_to_humidity_map.destination( x))
            .map(|x| humidity_to_location_map   .destination( x))
            .min()
            .unwrap();
        println!("Part 1: {part1}"); // 579439039
    
        let mut part2 = usize::max_value();
        for (start,range) in seeds.iter().tuples() {
            let new = (*start..(*start + *range))
                .into_par_iter()
                .map(|x| seed_to_soil_map           .destination( x))
                .map(|x| soil_to_fertilizer_map     .destination( x))
                .map(|x| fertilizer_to_water_map    .destination( x))
                .map(|x| water_to_light_map         .destination( x))
                .map(|x| light_to_temperature_map   .destination( x))
                .map(|x| temperature_to_humidity_map.destination( x))
                .map(|x| humidity_to_location_map   .destination( x))
                .min()
                .unwrap();
            part2 = std::cmp::min(part2,new);
        }
        println!("Part 2: {part2}"); // 7873084
    
        Ok(())
    }
    
    fn main() {
        let args: Vec<String> = env::args().collect();
        let filename = &args[1];
        solve(&filename).unwrap();
    }
    
    1 vote
  14. spit-evil-olive-tips
    Link
    part 1 package day05 import ( "fmt" "strings" "github.com/dlclark/regexp2" "github.com/spf13/cobra" "spit-evil-olive.tips/aoc2023/common" ) type PlantMap struct { Source int Target int Length int...
    part 1
    package day05
    
    import (
        "fmt"
        "strings"
    
        "github.com/dlclark/regexp2"
        "github.com/spf13/cobra"
    
        "spit-evil-olive.tips/aoc2023/common"
    )
    
    type PlantMap struct {
        Source int
        Target int
        Length int
    }
    
    type PlantMapping []PlantMap
    
    type Almanac struct {
        Seeds []int
    
        SeedSoil            PlantMapping
        SoilFertilizer      PlantMapping
        FertilizerWater     PlantMapping
        WaterLight          PlantMapping
        LightTemperature    PlantMapping
        TemperatureHumidity PlantMapping
        HumidityLocation    PlantMapping
    }
    
    var seedsRegex = regexp2.MustCompile(`seeds: (?<seeds>[\d ]+)`, regexp2.None)
    
    var mapNameRegex = regexp2.MustCompile(`(?<name>[\w-]+) map:`, regexp2.None)
    
    func parseSeeds(seedsLine string) ([]int, error) {
        var seeds []int
    
        match, err := seedsRegex.FindStringMatch(seedsLine)
        if err != nil {
            return seeds, err
        }
    
        if match == nil {
            return seeds, fmt.Errorf("failed to parse %v", seedsLine)
        }
    
        return common.ParseLineOfNumbers(match.GroupByName("seeds").String())
    }
    
    func parseMapping(mapDesc string) (string, PlantMapping, error) {
        var name string
        var mapping PlantMapping
    
        lines := strings.Split(mapDesc, "\n")
    
        match, err := mapNameRegex.FindStringMatch(lines[0])
        if err != nil {
            return name, mapping, err
        }
    
        if match == nil {
            return name, mapping, fmt.Errorf("failed to parse %v", mapDesc)
        }
    
        name = match.GroupByName("name").String()
    
        for _, line := range lines[1:] {
            parts, err := common.ParseLineOfNumbers(line)
            if err != nil {
                return name, mapping, err
            }
    
            pm := PlantMap{
                Target: parts[0],
                Source: parts[1],
                Length: parts[2],
            }
    
            mapping = append(mapping, pm)
        }
    
        return name, mapping, nil
    }
    
    func parseInput(path string) (*Almanac, error) {
        var a Almanac
    
        parts, err := common.ReadFileParts(path, "\n\n")
        if err != nil {
            return nil, err
        }
    
        a.Seeds, err = parseSeeds(parts[0])
        if err != nil {
            return nil, err
        }
    
        for _, mapDesc := range parts[1:] {
            name, mapping, err := parseMapping(mapDesc)
            if err != nil {
                return nil, err
            }
    
            switch name {
            case "seed-to-soil":
                a.SeedSoil = mapping
            case "soil-to-fertilizer":
                a.SoilFertilizer = mapping
            case "fertilizer-to-water":
                a.FertilizerWater = mapping
            case "water-to-light":
                a.WaterLight = mapping
            case "light-to-temperature":
                a.LightTemperature = mapping
            case "temperature-to-humidity":
                a.TemperatureHumidity = mapping
            case "humidity-to-location":
                a.HumidityLocation = mapping
            }
        }
    
        return &a, nil
    }
    
    func (mapping PlantMapping) Convert(value int) int {
        for _, pm := range mapping {
            offset := value - pm.Source
            if offset >= 0 && offset <= pm.Length {
                return pm.Target + offset
            }
        }
    
        return value
    }
    
    func (a *Almanac) GetLocation(seed int) int {
        soil := a.SeedSoil.Convert(seed)
        fertilizer := a.SoilFertilizer.Convert(soil)
        water := a.FertilizerWater.Convert(fertilizer)
        light := a.WaterLight.Convert(water)
        temperature := a.LightTemperature.Convert(light)
        humidity := a.TemperatureHumidity.Convert(temperature)
        location := a.HumidityLocation.Convert(humidity)
    
        return location
    }
    
    var CommandA = &cobra.Command{
        Use:  "05a",
        Args: cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            a, err := parseInput(args[0])
            if err != nil {
                return err
            }
    
            lowestLocation := 0
    
            for _, seed := range a.Seeds {
                location := a.GetLocation(seed)
                if lowestLocation == 0 || location < lowestLocation {
                    lowestLocation = location
                }
            }
    
            fmt.Printf("%v\n", lowestLocation)
    
            return nil
        },
    }
    
    part 2

    the first real AoC curveball of the year, the good ol' "oh yeah the brute-force solution will work, but go ahead and try to figure out how much time it'll take"

    I introduced a Range type, refactored my parsing logic to treat the seeds as a list of Ranges, and sorted the Ranges by starting point. I wasn't sure if I would need this in my range-mapping logic but I thought it might simplify it:

    --- day05/day05a.go     2023-12-05 21:57:03.081825549 -0800
    +++ day05/day05b.go     2023-12-05 22:01:06.274919925 -0800
    @@ -2,6 +2,8 @@
    
     import (
            "fmt"
    +       "math"
    +       "sort"
            "strings"
    
            "github.com/dlclark/regexp2"
    @@ -16,10 +18,15 @@
            Length int
     }
    
    +type Range struct {
    +       Start  int
    +       Length int
    +}
    +
     type PlantMapping []PlantMap
    
     type Almanac struct {
    -       Seeds []int
    +       Seeds []Range
    
            SeedSoil            PlantMapping
            SoilFertilizer      PlantMapping
    @@ -34,19 +41,29 @@
    
     var mapNameRegex = regexp2.MustCompile(`(?<name>[\w-]+) map:`, regexp2.None)
    
    -func parseSeeds(seedsLine string) ([]int, error) {
    -       var seeds []int
    +func parseSeeds(seedsLine string) ([]Range, error) {
    +       var seedRanges []Range
    
            match, err := seedsRegex.FindStringMatch(seedsLine)
            if err != nil {
    -               return seeds, err
    +               return seedRanges, err
            }
    
            if match == nil {
    -               return seeds, fmt.Errorf("failed to parse %v", seedsLine)
    +               return seedRanges, fmt.Errorf("failed to parse %v", seedsLine)
            }
    
    -       return common.ParseLineOfNumbers(match.GroupByName("seeds").String())
    +       seeds, err := common.ParseLineOfNumbers(match.GroupByName("seeds").String())
    +       if err != nil {
    +               return seedRanges, err
    +       }
    +
    +       for i := 0; i < len(seeds); i += 2 {
    +               start, length := seeds[i], seeds[i+1]
    +               seedRanges = append(seedRanges, Range{start, length})
    +       }
    +
    +       return seedRanges, nil
     }
    
     func parseMapping(mapDesc string) (string, PlantMapping, error) {
    @@ -81,6 +98,8 @@
                    mapping = append(mapping, pm)
            }
    
    +       sort.Slice(mapping, func(i, j int) bool { return mapping[i].Source < mapping[j].Source })
    +
            return name, mapping, nil
     }
    

    I added ParseLineOfNumbers to my common package, since I found myself repeating this logic:

    func ParseLineOfNumbers(input string) ([]int, error) {
        var numbers []int
    
        for _, s := range strings.Fields(input) {
            n, err := strconv.Atoi(s)
            if err != nil {
                return numbers, err
            }
            numbers = append(numbers, n)
        }
    
        return numbers, nil
    }
    

    this was the real hard part:

    func intMin(x, y int) int {
        return int(math.Min(float64(x), float64(y)))
    }
    
    func intMax(x, y int) int {
        return int(math.Max(float64(x), float64(y)))
    }
    
    func (mapping PlantMapping) ConvertRanges(sourceRanges []Range) []Range {
        var targetRanges []Range
    
        pending := make([]Range, len(sourceRanges))
        copy(pending, sourceRanges)
    
        for i := 0; i < len(pending); i++ {
            source := pending[i]
            rangeStart, rangeEnd := source.Start, source.Start+source.Length
            matched := false
    
            for _, pm := range mapping {
                offset := pm.Target - pm.Source
                mapStart, mapEnd := pm.Source, (pm.Source + pm.Length)
    
                //  |-range-|
                //             |-map-|
                if rangeEnd <= mapStart {
                    continue
                }
    
                //            |-range-|
                //  |-map-|
                if mapEnd <= rangeStart {
                    continue
                }
    
                //    |-range-|
                //  |----map----|
                //
                //  |---range---|
                //     |-map-|
                //
                //  |-range-|
                //        |-map-|
                //
                //      |-range-|
                //  |-map-|
                beforeLength := mapStart - rangeStart
                if beforeLength > 0 {
                    before := Range{rangeStart, beforeLength}
                    targetRanges = append(targetRanges, before)
                }
    
                middleStart := intMax(mapStart, rangeStart)
                middleEnd := intMin(mapEnd, rangeEnd)
                middleLength := middleEnd - middleStart
                if middleLength > 0 {
                    middle := Range{middleStart + offset, middleLength}
                    targetRanges = append(targetRanges, middle)
                }
    
                afterLength := rangeEnd - mapEnd
                if afterLength > 0 {
                    after := Range{Start: mapEnd, Length: afterLength}
                    pending = append(pending, after)
                }
    
                matched = true
                break
            }
    
            if !matched {
                target := Range{rangeStart, source.Length}
                targetRanges = append(targetRanges, target)
            }
        }
    
        return targetRanges
    }
    

    unit testing this was very helpful:

    type TestCase struct {
        input []Range
        expected []Range
    }
    
    func TestRangeConversion(t *testing.T) {
        assert := assert.New(t)
    
        // mapping from 10-20 and 40-60
        var mapping = PlantMapping{
            PlantMap{10, 1000, 10},
            PlantMap{40, 2000, 20},
        }
    
        testCases := []TestCase {
            TestCase{
                input: []Range {Range{0, 10}},
                expected: []Range {Range{0, 10}},
            },
            TestCase{
                input: []Range {Range{0, 15}},
                expected: []Range {Range{0, 10}, Range{1000, 5}},
            },
            TestCase{
                input: []Range {Range{15, 10}},
                expected: []Range {Range{1005, 5}, Range{20, 5}},
            },
            TestCase{
                input: []Range {Range{0, 25}},
                expected: []Range {Range{0, 10}, Range{1000, 10}, Range{20, 5}},
            },
            TestCase{
                input: []Range {Range{45, 10}},
                expected: []Range {Range{2005, 10}},
            },
            TestCase{
                input: []Range {Range{70, 20}},
                expected: []Range {Range{70, 20}},
            },
        }
    
        for _, tc := range testCases {
            actual := mapping.ConvertRanges(tc.input)
            assert.Equal(tc.expected, actual)
        }
    }
    

    once I worked out all the goddamn edge cases, of that, the rest was easy:

    func (a *Almanac) GetLocationRanges(seeds []Range) []Range {
        soil := a.SeedSoil.ConvertRanges(seeds)
        fertilizer := a.SoilFertilizer.ConvertRanges(soil)
        water := a.FertilizerWater.ConvertRanges(fertilizer)
        light := a.WaterLight.ConvertRanges(water)
        temperature := a.LightTemperature.ConvertRanges(light)
        humidity := a.TemperatureHumidity.ConvertRanges(temperature)
        location := a.HumidityLocation.ConvertRanges(humidity)
    
        return location
    }
    
    var CommandB = &cobra.Command{
        Use:  "05b",
        Args: cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            a, err := parseInput(args[0])
            if err != nil {
                return err
            }
    
            locationRanges := a.GetLocationRanges(a.Seeds)
    
            lowestLocation := math.MaxInt64
    
            for _, locationRange := range locationRanges {
                lowestLocation = intMin(lowestLocation, locationRange.Start)
            }
    
            fmt.Printf("%v\n", lowestLocation)
    
            return nil
        },
    }
    

    the most annoying, forehead-slapping bug I had along the way:

    func intMin(x, y int) int {
        return int(math.Min(float64(x), float64(x)))
    }
    
    func intMax(x, y int) int {
        return int(math.Max(float64(x), float64(x)))
    }
    
    performance
    > time ./aoc2023 05a day05/05.txt > /dev/null
    
    ________________________________________________________
    Executed in    2.53 millis    fish           external
       usr time    1.29 millis    0.00 micros    1.29 millis
       sys time    1.65 millis  359.00 micros    1.29 millis
    
    > time ./aoc2023 05b day05/05.txt > /dev/null
    
    ________________________________________________________
    Executed in    2.51 millis    fish           external
       usr time    0.83 millis    0.00 micros    0.83 millis
       sys time    2.07 millis  406.00 micros    1.66 millis
    
    1 vote
  15. lily
    Link
    I was going to wait to post here until I wrote a solution that wasn't brute force, but I haven't been able to. I figured out what I'm supposed to do, but for some reason I can only get my program...

    I was going to wait to post here until I wrote a solution that wasn't brute force, but I haven't been able to. I figured out what I'm supposed to do, but for some reason I can only get my program to work with either the test input or the proper input, not both. The only way I can get the correct answer for the proper input is by forcing my program to have what I believe is the incorrect behavior? It's really weird. Since it's been a day and I haven't figured it out, though, I'll just post my brute force solution from last night.

    Solution
    # Advent of Code 2023
    # Day 5: If You Give A Seed A Fertilizer
    
    mappings = []
    
    with open("inputs/day_5.txt") as file:
        current = {}
        waiting_for_nums = True
    
        for i, line in enumerate(file):
            if i == 0:
                seeds = [int(n) for n in line.split(": ")[1].split()]
            else:
                if line[0] in "0123456789":
                    nums = [int(n) for n in line.split()]
                    current[range(nums[1], nums[1] + nums[2])] = nums[0] - nums[1]
                    waiting_for_nums = False
                elif not waiting_for_nums:
                    mappings.append(current)
                    current = {}
                    waiting_for_nums = True
    
        mappings.append(current)
    
    def find_lowest_location(seeds, mappings):
        lowest_location = None
    
        for seed in seeds:
            current = seed
    
            for depth in mappings:
                for range_ in depth:
                    if current in range_:
                        current += depth[range_]
                        break
    
            if lowest_location is None or current < lowest_location:
                lowest_location = current
    
        return lowest_location
    
    print(f"Part 1: {find_lowest_location(seeds, mappings)}")
    
    def seeds_part_2():
        for start, length in zip(seeds[0::2], seeds[1::2]):
            for seed in range(start, start + length):
                yield seed
    
    print(f"Part 2: {find_lowest_location(seeds_part_2(), mappings)}")
    
    1 vote
  16. csos95
    Link
    I had to hold off on doing part two until today because my brain felt like mush all yesterday and it was just not happening. XD I decided not to go the bruteforce route with this one. At first I...

    I had to hold off on doing part two until today because my brain felt like mush all yesterday and it was just not happening. XD

    I decided not to go the bruteforce route with this one.
    At first I tried figuring out the different range overlap cases myself, but I eventually gave up and used the ranges library to calculate them.

    Part one takes 52.506μs and part two takes 114.60μs on my desktop with a ryzen 2700x.

    Rust
    use itertools::Itertools;
    use ranges::{GenericRange, Relation};
    use std::{
        collections::BTreeMap,
        ops::{Bound::*, RangeBounds},
    };
    
    #[aoc(day5, part1)]
    fn part1(input: &str) -> usize {
        let mut sections = input.split("\n\n");
        let mut seeds: Vec<usize> = sections
            .next()
            .unwrap()
            .strip_prefix("seeds: ")
            .unwrap()
            .split(" ")
            .map(|n| n.parse().unwrap())
            .collect();
    
        for section in sections {
            let section: BTreeMap<usize, (usize, usize)> =
                section
                    .split("\n")
                    .skip(1)
                    .fold(BTreeMap::new(), |mut mapping, line| {
                        let nums: Vec<usize> = line.split(" ").map(|n| n.parse().unwrap()).collect();
                        let dest = nums[0];
                        let src = nums[1];
                        let len = nums[2];
                        mapping.insert(src, (dest, len));
                        mapping
                    });
    
            seeds = seeds
                .into_iter()
                .map(|seed| {
                    let cursor = section.upper_bound(Included(&seed));
                    if let Some((src, (dest, len))) = cursor.key_value() {
                        if src + len > seed {
                            dest + (seed - src)
                        } else {
                            seed
                        }
                    } else {
                        seed
                    }
                })
                .collect();
        }
    
        seeds.into_iter().min().unwrap()
    }
    
    fn translate_range(range: GenericRange<usize>, offset: i64) -> GenericRange<usize> {
        let start = match range.start_bound() {
            Included(n) => Included(((*n as i64) + offset) as usize),
            Excluded(n) => Excluded(((*n as i64) + offset) as usize),
            Unbounded => Unbounded,
        };
        let end = match range.end_bound() {
            Included(n) => Included(((*n as i64) + offset) as usize),
            Excluded(n) => Excluded(((*n as i64) + offset) as usize),
            Unbounded => Unbounded,
        };
    
        GenericRange::new_with_bounds(start, end)
    }
    
    #[aoc(day5, part2)]
    fn part2(input: &str) -> usize {
        let mut sections = input.split("\n\n");
        let mut seeds: Vec<GenericRange<usize>> = sections
            .next()
            .unwrap()
            .strip_prefix("seeds: ")
            .unwrap()
            .split(" ")
            .map(|n| n.parse().unwrap())
            .chunks(2)
            .into_iter()
            .map(|chunk| {
                let (start, len) = chunk.collect_tuple().unwrap();
                GenericRange::new_closed_open(start, start + len)
            })
            .collect();
    
        for section in sections {
            let section: BTreeMap<usize, (usize, usize)> =
                section
                    .split("\n")
                    .skip(1)
                    .fold(BTreeMap::new(), |mut mapping, line| {
                        let nums: Vec<usize> = line.split(" ").map(|n| n.parse().unwrap()).collect();
                        let dest = nums[0];
                        let src = nums[1];
                        let len = nums[2];
                        mapping.insert(src, (dest, len));
                        mapping
                    });
    
            seeds = seeds
                .into_iter()
                .flat_map(|mut seed_range| {
                    let mut ranges = Vec::new();
                    let cursor = section.upper_bound(seed_range.start_bound());
                    let search_range = if let Some((src, _)) = cursor.key_value() {
                        GenericRange::new_with_bounds(Included(*src), seed_range.end_bound().cloned())
                    } else {
                        seed_range
                    };
    
                    let mut done = false;
                    for (src, (dest, len)) in section.range(search_range) {
                        let mapping_range = GenericRange::new_closed_open(*src, src + len);
                        let offset = (*dest as i64) - (*src as i64);
    
                        match seed_range.relation(mapping_range) {
                            Relation::Disjoint {
                                first, self_less, ..
                            } => {
                                if self_less {
                                    ranges.push(first);
                                    done = true;
                                    break;
                                }
                            }
                            Relation::Touching {
                                first, self_less, ..
                            } => {
                                if self_less {
                                    ranges.push(first);
                                    done = true;
                                    break;
                                }
                            }
                            Relation::Overlapping {
                                first_disjoint,
                                second_disjoint,
                                overlap,
                                self_less,
                                ..
                            } => {
                                if self_less {
                                    ranges.push(first_disjoint);
                                    ranges.push(translate_range(overlap, offset));
                                    done = true;
                                    break;
                                } else {
                                    ranges.push(translate_range(overlap, offset));
                                    seed_range = second_disjoint;
                                }
                            }
                            Relation::Containing {
                                first_disjoint,
                                second_disjoint,
                                overlap,
                                self_shorter,
                            } => {
                                if self_shorter {
                                    ranges.push(translate_range(overlap, offset));
                                    done = true;
                                    break;
                                } else {
                                    ranges.push(first_disjoint);
                                    ranges.push(translate_range(overlap, offset));
                                    seed_range = second_disjoint;
                                }
                            }
                            Relation::Starting {
                                overlap,
                                disjoint,
                                self_shorter,
                                ..
                            } => {
                                if self_shorter {
                                    ranges.push(translate_range(overlap, offset));
                                    done = true;
                                    break;
                                } else {
                                    ranges.push(translate_range(overlap, offset));
                                    seed_range = disjoint;
                                }
                            }
                            Relation::Ending {
                                disjoint,
                                overlap,
                                self_shorter,
                                ..
                            } => {
                                if self_shorter {
                                    ranges.push(translate_range(overlap, offset));
                                    done = true;
                                    break;
                                } else {
                                    ranges.push(disjoint);
                                    ranges.push(translate_range(overlap, offset));
                                    done = true;
                                    break;
                                }
                            }
                            Relation::Equal(range) => {
                                ranges.push(translate_range(range, offset));
                            }
                            Relation::Empty {
                                non_empty,
                                self_empty,
                            } => {
                                if let (Some(range), Some(true)) = (non_empty, self_empty) {
                                    ranges.push(range);
                                } else {
                                    done = true;
                                    break;
                                }
                            }
                        }
                    }
    
                    if !done {
                        ranges.push(seed_range);
                    }
                    ranges
                })
                .collect();
        }
    
        seeds
            .into_iter()
            .map(|range| match range.start_bound().cloned() {
                Included(n) => n,
                Excluded(n) => n,
                Unbounded => 0,
            })
            .min()
            .unwrap()
    }
    
    1 vote
  17. jzimbel
    Link
    Elixir I just got out of recursion hell implementing the optimized part 2 solution, and it's pretty wild to me that: the optimized approach solves part 2 in ~2.5ms when the naive one was still...

    Elixir

    I just got out of recursion hell implementing the optimized part 2 solution, and it's pretty wild to me that:

    • the optimized approach solves part 2 in ~2.5ms when the naive one was still chugging along after the better part of a day.
    • this is a day 4 puzzle! The optimization approach for this is similar to that of 2021's day 22 (!) puzzle. (Discussion and solution for that here, though there are probably easier-to-follow explanations out there by other folks.) Quaking in my boots at what's yet to come if we keep going at this rate.
    Messy part 1 and 2 solutions, featuring triple-nested Enum.reduce
    defmodule AdventOfCode.Solution.Year2023.Day05 do
      def part1(input), do: solve(input, &parse_seed_ranges_p1/1)
      def part2(input), do: solve(input, &parse_seed_ranges_p2/1)
    
      defp solve(input, seed_ranges_parser) do
        {seed_ranges, transforms} = parse(input, seed_ranges_parser)
    
        seed_ranges
        |> Stream.flat_map(&seed_range_to_location_ranges(&1, transforms))
        |> Stream.map(& &1.first)
        |> Enum.min()
      end
    
      defp parse(input, seed_ranges_parser) do
        [seeds_line | transform_lines] = String.split(input, "\n\n", trim: true)
    
        {seed_ranges_parser.(seeds_line), Enum.map(transform_lines, &parse_transform/1)}
      end
    
      defp parse_seed_ranges_p1("seeds: " <> seeds_str) do
        seeds_str
        |> String.split()
        |> Stream.map(&String.to_integer/1)
        |> Stream.map(&(&1..&1//1))
      end
    
      defp parse_seed_ranges_p2("seeds: " <> seeds_str) do
        seeds_str
        |> String.split()
        |> Stream.map(&String.to_integer/1)
        |> Stream.chunk_every(2)
        |> Stream.map(fn [start, len] -> start..(start + len - 1)//1 end)
      end
    
      defp parse_transform(transform_lines) do
        transform_lines
        |> String.split("\n", trim: true)
        |> Stream.drop(1)
        |> Stream.map(fn line ->
          line
          |> String.split()
          |> Enum.map(&String.to_integer/1)
        end)
        |> Enum.map(fn [dest_start, src_start, len] ->
          {src_start..(src_start + len - 1)//1, dest_start - src_start}
        end)
      end
    
      defp seed_range_to_location_ranges(seed_range, transforms) do
        Enum.reduce(transforms, [seed_range], &apply_transform/2)
      end
    
      defp apply_transform(transform_set, seed_ranges) do
        Enum.reduce_while(transform_set, %{unshifted: seed_ranges, shifted: []}, fn
          _, %{unshifted: []} = acc ->
            {:halt, acc}
    
          {window, shift_by}, acc ->
            updates =
              acc.unshifted
              |> Enum.reduce(%{unshifted: [], shifted: []}, fn seed_range, acc ->
                cond do
                  Range.disjoint?(window, seed_range) ->
                    Map.update!(acc, :unshifted, &[seed_range | &1])
    
                  window == seed_range ->
                    Map.update!(acc, :shifted, &[Range.shift(seed_range, shift_by) | &1])
    
                  true ->
                    {shifted_range, unshifted_ranges} =
                      intersect_and_shift(seed_range, window, shift_by)
    
                    acc
                    |> Map.update!(:shifted, &[shifted_range | &1])
                    |> Map.update!(:unshifted, &(unshifted_ranges ++ &1))
                end
              end)
    
            acc
            |> Map.put(:unshifted, updates.unshifted)
            |> Map.update!(:shifted, &(updates.shifted ++ &1))
            |> then(&{:cont, &1})
        end)
        |> then(&(&1.unshifted ++ &1.shifted))
      end
    
      defp intersect_and_shift(sl..sr//1, tl..tr//1, shift_by) do
        {
          Range.shift(max(sl, tl)..min(sr, tr)//1, shift_by),
          Enum.reject([sl..(tl - 1)//1, (tr + 1)..sr//1], &(Range.size(&1) == 0))
        }
      end
    end
    
    1 vote
  18. Toric
    Link
    just realizing I forgot to put my solution out there when actually got it done... Day 5 part 1 was straightforward, though kinda tedious to implement. Part 2 actually took me till the next day...

    just realizing I forgot to put my solution out there when actually got it done...

    Day 5 part 1 was straightforward, though kinda tedious to implement. Part 2 actually took me till the next day before I came up with a solution, but I ended up with a 1.2 ms solve for both parts! Parsing is probably taking more time than the math is!

    https://git.venberg.xyz/Gabe/advent_of_code_2023/src/branch/main/days/day05

    1 vote
  19. whispersilk
    Link
    Rust At first I did things the brute-force way, which was fine for part 1 but resulted in part 2 taking 85 seconds(!) to run — given that it was doing calculations for some 1.6 billion seeds on a...

    Rust

    At first I did things the brute-force way, which was fine for part 1 but resulted in part 2 taking 85 seconds(!) to run — given that it was doing calculations for some 1.6 billion seeds on a single core, that's respectable, but I want things to run quickly. With some imports I could have made it multi-core, but I'm still sticking to std and that wouldn't have gotten it down to where I wanted anyway.

    In the end, though, I figured out the same technique of collapsing the layers into one so that the actual comparisons can be done both intelligently and all at once that it seems most people used. Getting all the edge cases nailed out was rough, but I got it and in the end both parts combined run in just under 200 μs.

    Of course, between implementing the smart solution and just not having much time the past few days I'm now a bit behind on the other days, so I have some catching up to do.

    Parser and data structure — sorry, this one's ugly
    #[derive(Debug, Clone, Copy)]
    struct Range {
        input_start: usize,
        input_end: usize,
        output_start: usize,
        output_end: usize,
    }
    
    enum CombinedRange {
        Input(Range),
        Output(Range),
        Overlap(Range),
        None,
    }
    
    impl Range {
        fn new(input_start: usize, input_end: usize, output_start: usize, output_end: usize) -> Self {
            Self { input_start, input_end, output_start, output_end }
        }
    
    
        fn translate(&self, val: usize) -> Option<usize> {
            if val >= self.input_start && val <= self.input_end {
                if self.input_start > self.output_start {
                    Some(val - (self.input_start - self.output_start))
                } else {
                    Some(val + (self.output_start - self.input_start))
                }
            } else {
                None
            }
        }
    
        fn translate_min(&self, range: (usize, usize)) -> Option<usize> {
            if range.1 < self.input_start || range.0 > self.input_end {
                None
            } else {
                self.translate(std::cmp::max(range.0, self.input_start))
            }
        }
    
        fn overlaps(input: &Range, output: &Range) -> bool {
            input.output_end >= output.input_start && input.output_start <= output.input_end
        }
    
        fn input_for(&self, output: usize) -> usize {
            let offset = output - self.output_start;
            self.input_start + offset
        }
    
        fn output_for(&self, input: usize) -> usize {
            let offset = input - self.input_start;
            self.output_start + offset
        }
    
        // Splits an input range based on a value in its output range.
        // The provided value is part of the SECOND range returned, if applicable.
        fn split_at_output(self, second_start: usize) -> (Option<Range>, Option<Range>) {
            if second_start <= self.output_start {
                (None, Some(self))
            } else if second_start > self.output_end {
                (Some(self), None)
            } else {
                let first_len = second_start - 1 - self.output_start;
                let first = Range::new(self.input_start, self.input_start + first_len, self.output_start, self.output_start + first_len);
                let second = Range::new(first.input_end + 1, self.input_end, first.output_end + 1, self.output_end);
                (Some(first), Some(second))
            }
        }
    
        fn combine(input: Range, output: Range) -> [CombinedRange; 3] {
            use CombinedRange::*;
            use std::cmp::Ordering;
            if !Range::overlaps(&input, &output) {
                [Input(input), Output(output), None]
            } else {
                let (low, high) = (
                    std::cmp::max(input.output_start, output.input_start),
                    std::cmp::min(input.output_end, output.input_end),
                );
                let overlap = Overlap(Range::new(input.input_for(low), input.input_for(high), output.output_for(low), output.output_for(high)));
                match input.output_start.cmp(&output.input_start) {
                    Ordering::Less => {
                        let first = Input(Range::new(input.input_start, input.input_for(low - 1), input.output_start, low - 1));
                        match input.output_end.cmp(&output.input_end) {
                            Ordering::Less => [
                                first,
                                overlap,
                                Output(Range::new(high + 1, output.input_end, output.output_for(high + 1), output.output_end)),
                            ],
                            Ordering::Equal => [
                                first,
                                overlap,
                                None,
                            ],
                            Ordering::Greater => [
                                first,
                                overlap,
                                Input(Range::new(input.input_for(high + 1), input.input_end, high + 1, input.output_end)),
                            ],
                        }
                    }
                    Ordering::Equal => match input.output_end.cmp(&output.input_end) {
                        Ordering::Less => [
                            overlap,
                            Output(Range::new(high + 1, output.input_end, output.output_for(high + 1), output.output_end)),
                            None,
                        ],
                        Ordering::Equal => [
                            overlap,
                            None,
                            None,
                        ],
                        Ordering::Greater => [
                            overlap,
                            Input(Range::new(input.input_for(high + 1), input.input_end, high + 1, input.output_end)),
                            None,
                        ],
                    }
                    Ordering::Greater => {
                        let first = Output(Range::new(output.input_start, low - 1, output.output_start, output.output_for(low - 1)));
                        match input.output_end.cmp(&output.input_end) {
                            Ordering::Less => [
                                first,
                                overlap,
                                Output(Range::new(high + 1, output.input_end, output.output_for(high + 1), output.output_end)),
                            ],
                            Ordering::Equal => [
                                first,
                                overlap,
                                None,
                            ],
                            Ordering::Greater => [
                                first,
                                overlap,
                                Input(Range::new(input.input_for(high + 1), input.input_end, high + 1, input.output_end)),
                            ],
                        }
                    }
                }
            }
        }
    }
    
    fn parse_input() -> Result<(Vec<usize>, Vec<Range>)> {
        let input = load_input(5)?;
        let (seeds, maps) = input.split_once("\n\n").ok_or("No section divider")?;
        let seeds = seeds
            .strip_prefix("seeds: ")
            .ok_or("No seeds prefix")?
            .split(' ')
            .map(parse_as::<usize>)
            .collect::<Result<Vec<usize>>>()?;
        let map = maps
            .split("\n\n")
            .try_fold(Vec::new(), |prior, section| {
                let mut ranges = section
                    .split('\n')
                    .skip(1)
                    .filter(|line| !line.is_empty())
                    .map(|line| {
                        let nums = line
                            .split(' ')
                            .map(parse_as::<usize>)
                            .collect::<Result<Vec<usize>>>()?;
                        if let [dest, source, len] = nums.as_slice() {
                            Ok(Range::new(*source, source + len - 1, *dest, dest + len - 1))
                        } else {
                            Err(format!("Wrong number of elements on line {line}").into())
                        }
                    })
                    .collect::<Result<Vec<Range>>>()?;
                let mut new_ranges = Vec::new();
                for mut input in prior {
                    let mut outputs = ranges
                        .iter()
                        .enumerate()
                        .filter(|(_, output)| Range::overlaps(&input, output))
                        .map(|(idx, _)| idx)
                        .collect::<Vec<_>>();
                    outputs.sort_unstable();
                    let mut outputs = outputs
                        .into_iter()
                        .rev()
                        .map(|idx| ranges.remove(idx))
                        .collect::<Vec<_>>();
                    outputs.sort_unstable_by_key(|range| range.input_start);
                    if outputs.len() > 1 {
                        for output in outputs {
                            match input.split_at_output(output.input_end + 1) {
                                (Some(overlaps), Some(after)) => {
                                    input = after;
                                    for range in Range::combine(overlaps, output) {
                                        match range {
                                            CombinedRange::Input(x) | CombinedRange::Overlap(x) => new_ranges.push(x),
                                            CombinedRange::Output(x) => ranges.push(x),
                                            CombinedRange::None => (),
                                        }
                                    }
                                }
                                (Some(overlaps), None) => {
                                    for range in Range::combine(overlaps, output) {
                                        match range {
                                            CombinedRange::Input(x) | CombinedRange::Overlap(x) => new_ranges.push(x),
                                            CombinedRange::Output(x) => ranges.push(x),
                                            CombinedRange::None => (),
                                        }
                                    }
                                }
                                (None, Some(_)) => unreachable!(),
                                (None, None) => unreachable!(),
                            }
                        }
                    } else if !outputs.is_empty() {
                        let output = outputs[0];
                        for range in Range::combine(input, output) {
                            match range {
                                CombinedRange::Input(x) | CombinedRange::Overlap(x) => new_ranges.push(x),
                                CombinedRange::Output(x) => ranges.push(x),
                                CombinedRange::None => (),
                            }
                        }
                    } else {
                        new_ranges.push(input);
                    }
                }
                new_ranges.extend(ranges);
                let output: Result<Vec<_>> = Ok(new_ranges);
                output
            })?;
    
        Ok((seeds, map))
    }
    
    Part 1
    fn first() -> Result<String> {
        let (seeds, ranges) = parse_input()?;
        let min_seed_location = seeds
            .into_iter()
            .map(|seed| ranges.iter().find_map(|range| range.translate(seed)).unwrap_or(seed))
            .min()
            .ok_or("There is at least one seed")?
            .to_string();
        Ok(min_seed_location)
    }
    
    Part 2
    fn second() -> Result<String> {
        let (seeds, ranges) = parse_input()?;
        let mut seed_ranges = Vec::new();
        for x in (1..seeds.len()).step_by(2) {
            seed_ranges.push((seeds[x - 1], seeds[x - 1] + seeds[x] + 1));
        }
    
        let min_seed_location = seed_ranges
            .into_iter()
            .map(|seed_range| ranges.iter().find_map(|range| range.translate_min(seed_range)).unwrap_or(seed_range.0))
            .min()
            .ok_or("There is at least one seed range")?
            .to_string();
        Ok(min_seed_location)
    }
    
    1 vote
  20. tjf
    Link
    Posting this a few days late because I waited to sit down and think through a non-bruteforce part two strategy. Not super clean but it does the thing (and doesn't take 45 minutes with 4 cores!)....

    Posting this a few days late because I waited to sit down and think through a non-bruteforce part two strategy. Not super clean but it does the thing (and doesn't take 45 minutes with 4 cores!). Here are my Python solutions:

    Part 1
    #!/usr/bin/env pypy3
    
    import math
    import sys
    
    seeds = map(int, sys.stdin.readline().split(':')[1].split())
    mappings = [[((src, src + length), dst - src)
                 for dst, src, length in (map(int, m.split())
                                          for m in mapgroup.split('\n')
                                          if m and m[0].isdigit())]
                for mapgroup in sys.stdin.read().split('\n\n')]
    
    lowest = math.inf
    for s in seeds:
        for mapgroup in mappings:
            for m, shift in mapgroup:
                if m[0] <= s < m[1]:
                    s += shift
                    break
        lowest = min(lowest, s)
    
    print(lowest)
    
    Part 2
    #!/usr/bin/env pypy3
    
    import sys
    
    def compare_intervals(a, b):
        left, right = (a[0], a[0]), (a[1], a[1])
        if (a[0] <= a[1] < b[0]) or (b[1] < a[0] <= a[1]):
            overlap = (0, 0)
            return left, overlap, right
        if a[0] < b[0]:
            left = (a[0], b[0])
        if a[1] > b[1]:
            right = (b[1], a[1])
        overlap = (left[1], right[0])
        return left, overlap, right
    
    def is_nonempty_interval(a):
        return a[0] != a[1]
    
    def shift_interval(a, shift):
        return (a[0] + shift, a[1] + shift)
    
    def update_interval(r, mapgroup):
        unmapped = set([r])
        mapped = set()
        while unmapped:
            rr = unmapped.pop()
            maps_to_self = True
            for m, shift in mapgroup:
                left, overlap, right = compare_intervals(rr, m)
                if is_nonempty_interval(overlap):
                    maps_to_self = False
                    mapped.add(shift_interval(overlap, shift))
                    unmapped.add(left)
                    unmapped.add(right)
            if maps_to_self:
                mapped.add(rr)
        return filter(is_nonempty_interval, mapped)
    
    def flatten(x):
        return [z for y in x for z in y]
    
    starts_and_lengths = map(int, sys.stdin.readline().split(':')[1].split())
    seed_ranges = [(start, start + next(starts_and_lengths))
                   for start in starts_and_lengths]
    mappings = [[((src, src + length), dst - src)
                 for dst, src, length in (map(int, m.split())
                                          for m in mapgroup.split('\n')
                                          if m and m[0].isdigit())]
                for mapgroup in sys.stdin.read().split('\n\n')]
    
    ranges = seed_ranges
    for mapgroup in mappings:
        ranges = flatten(update_interval(r, mapgroup) for r in ranges)
    
    print(min(ranges)[0])
    
    1 vote