Day 5: If You Give a Seed a Fertilizer
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- Code block support is not fully rolled out yet but likely will be in the middle of the event. Try to share solutions as both code blocks and using something such as https://topaz.github.io/paste/ , pastebin, or github (code blocks to future proof it for when 0.19 comes out and since code blocks currently function in some apps and some instances as well if they are running a 0.19 beta)
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
🔒This post will be unlocked when there is a decent amount of submissions on the leaderboard to avoid cheating for top spots
🔓 Unlocked after 27 mins (current record for time, hard one today)
Haskell
Not hugely proud of this one; part one would have been easier if I'd spend more time reading the question and not started on an overly-general solution, and I lost a lot of time on part two to a missing
a +
. More haste, less speed, eh?import Data.List import Data.List.Split readInput :: String -> ([Int], [(String, [(Int, Int, Int)])]) readInput s = let (seedsChunk : mapChunks) = splitOn [""] $ lines s seeds = map read $ tail $ words $ head seedsChunk maps = map readMapChunk mapChunks in (seeds, maps) where readMapChunk (title : rows) = let name = head $ words title entries = map ((\[a, b, c] -> (a, b, c)) . map read . words) rows in (name, entries) part1 (seeds, maps) = let f = foldl1' (flip (.)) $ map (ref . snd) maps in minimum $ map f seeds where ref [] x = x ref ((a, b, c) : rest) x = let i = x - b in if i >= 0 && i < c then a + i else ref rest x mapRange :: [(Int, Int, Int)] -> (Int, Int) -> [(Int, Int)] mapRange entries (start, end) = go start $ sortOn (\(_, b, _) -> b) entries where go i [] = [(i, end)] go i es@((a, b, c) : rest) | i > end = [] | b > end = go i [] | b + c <= i = go i rest | i < b = (i, b - 1) : go b es | otherwise = let d = min (b + c - 1) end in (a + i - b, a + d - b) : go (d + 1) rest part2 (seeds, maps) = let seedRanges = map (\[a, b] -> (a, a + b - 1)) $ chunksOf 2 seeds in minimum $ map fst $ foldl' (flip mapRanges) seedRanges $ map snd maps where mapRanges m = concatMap (mapRange m) main = do input <- readInput <$> readFile "input05" print $ part1 input print $ part2 input
Finally. :cries:
Part 1 was fine, I was figuring I might be able to practice classes.
Part 2 told me that nope, memory management required for you. In the end instead of calculating seeds, I resolved the whole thing down to a single mapping of seeds to locations. Then I could just sort by location ranges and try to see if they were a seed. Code is full of old parts from failed solutions but I've had enough of day 5, so I no longer care to clean it up.
Odin
When I read the problem description I expected the input to also be 2 digit numbers. When I looked at it I just had to say "huh."
Second part I think you definitely have to do in reverse (edit: if you are doing a linear search for the answer), as that allows you to nope out as soon as you find a match, whereas with doing it forward you have to keep checking just in case.
package day5 import "core:fmt" import "core:strings" import "core:slice" import "core:strconv" Range :: struct { dest: int, src: int, range: int, } Mapper :: struct { ranges: []Range, } parse_range :: proc(s: string) -> (ret: Range) { rest := s parseLen := -1 destOk: bool ret.dest, destOk = strconv.parse_int(rest, 10, &parseLen) rest = strings.trim_left_space(rest[parseLen:]) srcOk: bool ret.src, srcOk = strconv.parse_int(rest, 10, &parseLen) rest = strings.trim_left_space(rest[parseLen:]) rangeOk: bool ret.range, rangeOk = strconv.parse_int(rest, 10, &parseLen) return } parse_mapper :: proc(ss: []string) -> (ret: Mapper) { ret.ranges = make([]Range, len(ss)-1) for s, i in ss[1:] { ret.ranges[i] = parse_range(s) } return } parse_mappers :: proc(ss: []string) -> []Mapper { mapsStr := make([dynamic][]string) defer delete(mapsStr) restOfLines := ss isLineEmpty :: proc(s: string)->bool {return len(s)==0} for i, found := slice.linear_search_proc(restOfLines, isLineEmpty); found; i, found = slice.linear_search_proc(restOfLines, isLineEmpty) { append(&mapsStr, restOfLines[:i]) restOfLines = restOfLines[i+1:] } append(&mapsStr, restOfLines[:]) return slice.mapper(mapsStr[1:], parse_mapper) } apply_mapper :: proc(mapper: Mapper, num: int) -> int { for r in mapper.ranges { if num >= r.src && num - r.src < r.range do return num - r.src + r.dest } return num } p1 :: proc(input: []string) { maps := parse_mappers(input) defer { for m in maps do delete(m.ranges) delete(maps) } restSeeds := input[0][len("seeds: "):] min := 0x7fffffff for len(restSeeds) > 0 { seedLen := -1 seed, seedOk := strconv.parse_int(restSeeds, 10, &seedLen) restSeeds = strings.trim_left_space(restSeeds[seedLen:]) fmt.print(seed) for m in maps { seed = apply_mapper(m, seed) fmt.print(" ->", seed) } fmt.println() if seed < min do min = seed } fmt.println(min) } apply_mapper_reverse :: proc(mapper: Mapper, num: int) -> int { for r in mapper.ranges { if num >= r.dest && num - r.dest < r.range do return num - r.dest + r.src } return num } p2 :: proc(input: []string) { SeedRange :: struct { start: int, len: int, } seeds := make([dynamic]SeedRange) restSeeds := input[0][len("seeds: "):] for len(restSeeds) > 0 { seedLen := -1 seedS, seedSOk := strconv.parse_int(restSeeds, 10, &seedLen) restSeeds = strings.trim_left_space(restSeeds[seedLen:]) seedL, seedLOk := strconv.parse_int(restSeeds, 10, &seedLen) restSeeds = strings.trim_left_space(restSeeds[seedLen:]) append(&seeds, SeedRange{seedS, seedL}) } maps := parse_mappers(input) defer { for m in maps do delete(m.ranges) delete(maps) } for i := 0; true; i += 1 { rseed := i #reverse for m in maps { rseed = apply_mapper_reverse(m, rseed) } found := false for sr in seeds { if rseed >= sr.start && rseed < sr.start + sr.len { found = true break } } if found { fmt.println(i) break } } }
[JavaScript] Well that was by far the hardest out of all of the days, part 1 was relatively fine but part 2 took me awhile of trying different things
Ended up solving it by working backwards by trying different location values and seeing if that can become a valid seed. Takes around 3 secs to compute the answer.
Part 1 Code Block
// Part 1 // ====== function part1(input) { const split = input.split("\r\n\r\n"); let pastValues = split[0].match(/\d+/g).map((x) => parseInt(x)); let currentValues = []; for (const section of split.slice(1)) { for (const line of section.split("\r\n")) { const values = line.match(/\d+/g)?.map((x) => parseInt(x)); if (!values) { continue; } const sourceStart = values[1]; const destinationStart = values[0]; const length = values[2]; for (let i = 0; i < pastValues.length; i++) { if ( pastValues[i] >= sourceStart && pastValues[i] < sourceStart + length ) { currentValues.push(destinationStart + pastValues[i] - sourceStart); pastValues.splice(i, 1); i--; } } } for (let i = 0; i < pastValues.length; i++) { currentValues.push(pastValues[i]); } pastValues = [...currentValues]; currentValues = []; } return Math.min(...pastValues); }
Part 2 Code Block
// Part 2 // ====== function part2(input) { const split = input.split("\r\n\r\n"); let seeds = split[0].match(/\d+/g).map((x) => parseInt(x)); seeds = seeds .filter((x, i) => i % 2 == 0) .map((x, i) => [x, seeds[i * 2 + 1]]); const maps = split .slice(1) .map((x) => { const lines = x.split("\r\n"); return lines .map((x) => x.match(/\d+/g)?.map((x) => parseInt(x))) .filter((x) => x); }) .reverse(); for (let i = 0; true; i++) { let curValue = i; for (const map of maps) { for (const line of map) { const sourceStart = line[1]; const destinationStart = line[0]; const length = line[2]; if ( curValue >= destinationStart && curValue < destinationStart + length ) { curValue = sourceStart + curValue - destinationStart; break; } } } for (const [seedRangeStart, seedRangeLength] of seeds) { if ( curValue >= seedRangeStart && curValue < seedRangeStart + seedRangeLength ) { return i; } } } }
Torn between doing the problem backwards and implementing a more general case -- glad to know both approaches work out in the end!
Like many others, I really didn't enjoy this one. I particularly struggled with part 02, which ended up with me just brute forcing it and checking each seed. On my system it took over 15 minutes to run, which is truly awful. I'm open to pointers on how I could better have solved part two.
Solution in Rust 🦀
Code
use std::{ env, fs, io::{self, BufReader, Read}, }; fn main() -> io::Result<()> { let args: Vec = env::args().collect(); let filename = &args[1]; let file1 = fs::File::open(filename)?; let file2 = fs::File::open(filename)?; let mut reader1 = BufReader::new(file1); let mut reader2 = BufReader::new(file2); println!("Part one: {}", process_part_one(&mut reader1)); println!("Part two: {}", process_part_two(&mut reader2)); Ok(()) } #[derive(Debug)] struct Map { lines: Vec, } impl Map { fn map_to_lines(&self, key: u32) -> u32 { for line in &self.lines { if line.in_range(key) { return line.map(key); } } key } } #[derive(Debug)] struct MapLine { dest_range: u32, source_range: u32, range_length: u32, } impl MapLine { fn map(&self, key: u32) -> u32 { let diff = key - self.source_range; if self.dest_range as i64 + diff as i64 > 0 { return (self.dest_range as i64 + diff as i64) as u32; } key } fn in_range(&self, key: u32) -> bool { self.source_range <= key && (key as i64) < self.source_range as i64 + self.range_length as i64 } } fn parse_input(reader: &mut BufReader) -> (Vec, Vec<map>) { let mut almanac = String::new(); reader .read_to_string(&mut almanac) .expect("read successful"); let parts: Vec<&str> = almanac.split("\n\n").collect(); let (seeds, others) = parts.split_first().expect("at least one part"); let seeds: Vec<_> = seeds .split(": ") .last() .expect("at least one") .split_whitespace() .map(|s| s.to_string()) .collect(); let maps: Vec<_> = others .iter() .map(|item| { let lines_iter = item .split(':') .last() .expect("exists") .trim() .split('\n') .map(|nums| { let nums_split = nums.split_whitespace().collect::>(); MapLine { dest_range: nums_split[0].parse().expect("is digit"), source_range: nums_split[1].parse().expect("is digit"), range_length: nums_split[2].parse().expect("is digit"), } }); Map { lines: lines_iter.collect(), } }) .collect(); (seeds, maps) } fn process_part_one(reader: &mut BufReader) -> u32 { let (seeds, maps) = parse_input(reader); let mut res = u32::MAX; for seed in &seeds { let mut val = seed.parse::().expect("is digits"); for map in &maps { val = map.map_to_lines(val); } res = u32::min(res, val); } res } fn process_part_two(reader: &mut BufReader) -> u32 { let (seeds, maps) = parse_input(reader); let seed_chunks: Vec<_> = seeds.chunks(2).collect(); let mut res = u32::MAX; for chunk in seed_chunks { let range_start: u32 = chunk[0].parse().expect("is digits"); let range_length: u32 = chunk[1].parse().expect("is digits"); let range_end: u32 = range_start + range_length; for seed in range_start..range_end { let mut val = seed; for map in &maps { val = map.map_to_lines(val); } res = u32::min(res, val); } } res } #[cfg(test)] mod tests { use super::*; const INPUT: &str = "seeds: 79 14 55 13 seed-to-soil map: 50 98 2 52 50 48 soil-to-fertilizer map: 0 15 37 37 52 2 39 0 15 fertilizer-to-water map: 49 53 8 0 11 42 42 0 7 57 7 4 water-to-light map: 88 18 7 18 25 70 light-to-temperature map: 45 77 23 81 45 19 68 64 13 temperature-to-humidity map: 0 69 1 1 0 69 humidity-to-location map: 60 56 37 56 93 4"; #[test] fn test_process_part_one() { let input_bytes = INPUT.as_bytes(); assert_eq!(35, process_part_one(&mut BufReader::new(input_bytes))); } #[test] fn test_process_part_two() { let input_bytes = INPUT.as_bytes(); assert_eq!(46, process_part_two(&mut BufReader::new(input_bytes))); } }
:::</map>
CRYSTAL
finally solved part 2, and in just 1 second :)))
Input = File.read("input.txt").lines # {source, destination} alias Map = Tuple(Range(Int64, Int64), Range(Int64, Int64)) Maps = Array(Array(Map)).new(7) index = 1 7.times do |i| a, index = get_ranges(index + 2) Maps << a end part2 # part1 def part1() seeds = Input[0].split(":")[1].split.map(&.to_i64) locs = Array(Int64).new(7) seeds.each do |seed| val = seed Maps.each do |maplist| maplist.each do |map| if map[0].includes?(val) val = map[1].begin + (val - map[0].begin) break end end end locs << val end puts locs.min end def part2() seeds = Input[0].split(":")[1].split.map(&.to_i64) seeds = seeds.in_groups_of(2, 0).map { |a| a[0]..(a[0]+a[1]) } found = false loc = 0 until found val = loc Maps.reverse_each do |maplist| maplist.each do |map| if map[1].includes?(val) val = map[0].begin + (val - map[1].begin) break end end end seeds.each { |seedrange| break if found = seedrange.includes?(val) } loc += 1 end puts loc - 1 end def get_ranges(index : Int) : {Array(Map), Int32} line = Input[index] ranges = [] of Map until line == "" a, b, l = line.split.map(&.to_i64) ranges << {b...(b+l), a...(a+l)} index += 1 break if index == Input.size line = Input[index] end {ranges, index} end
[Language: Lean4]
I'll only post the actual parsing and solution. I have written some helpers (in this case particularly relevant: Quicksort) which are in other files, as is the main function. For the full code, please see my github repo.
This one also ended up quite long, because I couldn't resist to use different types for the different things, and to have the type checker confirm that I'm combining the maps between them in the correct order.
Also, I am not 100% certain that part 2 doesn't have any off-by-one errors. I didn't write any unit tests for it... The answer is correct though, so I probably didn't mess it up too horribly. Also, it is pretty fast. Part 2 takes about 1.2 milliseconds on my machine, and this is including the file parsing (but not the loading of the file).
It seems my solution is too long for a single post though, so I'll split off part 2 and post it separately.
Edit: There was a bug in the function that checks overlaps between ranges while parsing.
Parsing and Part 1
structure Seed where id : Nat deriving BEq, Ord, Repr structure Soil where id : Nat deriving BEq, Ord, Repr structure Fertilizer where id : Nat deriving BEq, Ord, Repr structure Water where id : Nat deriving BEq, Ord, Repr structure Light where id : Nat deriving BEq, Ord, Repr structure Temperature where id : Nat deriving BEq, Ord, Repr structure Humidity where id : Nat deriving BEq, Ord, Repr structure Location where id : Nat deriving BEq, Ord, Repr private class NatId (α : Type) where fromNat : Nat → α toNat : α → Nat private instance : NatId Seed where fromNat := Seed.mk toNat := Seed.id private instance : NatId Soil where fromNat := Soil.mk toNat := Soil.id private instance : NatId Fertilizer where fromNat := Fertilizer.mk toNat := Fertilizer.id private instance : NatId Water where fromNat := Water.mk toNat := Water.id private instance : NatId Light where fromNat := Light.mk toNat := Light.id private instance : NatId Temperature where fromNat := Temperature.mk toNat := Temperature.id private instance : NatId Humidity where fromNat := Humidity.mk toNat := Humidity.id private instance : NatId Location where fromNat := Location.mk toNat := Location.id private instance : Min Location where min a b := if Ord.compare a b == Ordering.lt then a else b structure Mapping (α β : Type) where inputStart : α outputStart : β length : Nat deriving Repr structure Mappings (α β : Type) where mappings : List $ Mapping α β deriving Repr private def Mapping.apply? {α β : Type} [NatId α] [NatId β] (mapping : Mapping α β) (input : α) : Option β := let input := NatId.toNat input let fromStart := NatId.toNat mapping.inputStart let toStart := NatId.toNat mapping.outputStart if input ≥ fromStart ∧ input < fromStart + mapping.length then some $ NatId.fromNat $ toStart + input - fromStart else none private def Mappings.apply {α β : Type} [NatId α] [NatId β] (mappings : Mappings α β) (input : α) : β := let applied := mappings.mappings.findSome? $ flip Mapping.apply? input applied.getD $ NatId.fromNat $ NatId.toNat input structure Almanach where seedsToSoil : Mappings Seed Soil soilToFertilizer : Mappings Soil Fertilizer fertilizerToWater : Mappings Fertilizer Water waterToLight : Mappings Water Light lightToTemperature : Mappings Light Temperature temperatureToHumidity : Mappings Temperature Humidity humidityToLocation : Mappings Humidity Location deriving Repr private def parseSeeds (input : String) : Option (List Seed) := if input.startsWith "seeds: " then let input := input.drop 7 let input := String.trim <$> input.split Char.isWhitespace let numbers := input.mapM String.toNat? List.map NatId.fromNat <$> numbers else none private def parseMapping {α β : Type} [NatId α] [NatId β] (input : String) : Option $ Mapping α β := do let input := String.trim <$> input.split Char.isWhitespace let nums ← input.mapM String.toNat? match nums with | [a,b,c] => some $ {inputStart := NatId.fromNat b, outputStart := NatId.fromNat a, length := c} | _ => none private def Mapping.overlap {α β : Type} [NatId α] [NatId β] (a : Mapping α β) (b : Mapping α β) : Bool := let aStart := NatId.toNat $ a.inputStart let aEnd := aStart + a.length let bStart := NatId.toNat $ b.inputStart let bEnd := bStart + b.length (bStart ≥ aStart && bStart < aEnd) || (bEnd > aStart && bEnd ≤ aEnd) || (aStart ≥ bStart && aStart < bEnd) || (aEnd > bStart && aEnd ≤ bEnd) private def parseMappings (α β : Type) [NatId α] [NatId β] (input : String) (header : String) : Option $ Mappings α β := if input.startsWith header then let lines := String.trim <$> input.splitOn "\n" |> List.drop 1 |> List.filter (not ∘ String.isEmpty) let mappings := lines.mapM parseMapping let rec overlapHelper := λ (a : List $ Mapping α β) ↦ match a with | [] => false | a :: as => as.any (λ b ↦ a.overlap b) || overlapHelper as let mappings := mappings.filter $ not ∘ overlapHelper --make sure no ranges overlap. That would be faulty Mappings.mk <$> mappings else none def parse (input : String) : Option ((List Seed) × Almanach) := do let blocks := input.splitOn "\n\n" |> List.filter (not ∘ String.isEmpty) let blocks := String.trim <$> blocks if let [seeds, seedToSoil, soilToFertilizer, fertilizerToWater, waterToLight, lightToTemperature, temperatureToHumidity, humidityToLocation] := blocks then let seeds ← parseSeeds seeds let seedToSoil ← parseMappings Seed Soil seedToSoil "seed-to-soil map:" let soilToFertilizer ← parseMappings Soil Fertilizer soilToFertilizer "soil-to-fertilizer map:" let fertilizerToWater ← parseMappings Fertilizer Water fertilizerToWater "fertilizer-to-water map:" let waterToLight ← parseMappings Water Light waterToLight "water-to-light map:" let lightToTemperature ← parseMappings Light Temperature lightToTemperature "light-to-temperature map:" let temperatureToHumidity ← parseMappings Temperature Humidity temperatureToHumidity "temperature-to-humidity map:" let humidityToLocation ← parseMappings Humidity Location humidityToLocation "humidity-to-location map:" (seeds, { seedsToSoil := seedToSoil soilToFertilizer := soilToFertilizer fertilizerToWater := fertilizerToWater waterToLight := waterToLight lightToTemperature := lightToTemperature temperatureToHumidity := temperatureToHumidity humidityToLocation := humidityToLocation : Almanach}) else none def part1 (input : ((List Seed) × Almanach)) : Option Nat := let a := input.snd let seedToLocation := a.humidityToLocation.apply ∘ a.temperatureToHumidity.apply ∘ a.lightToTemperature.apply ∘ a.waterToLight.apply ∘ a.fertilizerToWater.apply ∘ a.soilToFertilizer.apply ∘ a.seedsToSoil.apply let locations := input.fst.map seedToLocation NatId.toNat <$> locations.minimum?
Part 2
private structure Mapping2 (α β : Type) where start : α --okay, next time I do this, I'll encode end and offset, not start and offset... offset : Int deriving Repr private structure Mappings2 (α β : Type) where mappings : List $ Mapping2 α β deriving Repr private def Mappings2.fromMappings {α β : Type} [NatId α] [NatId β] [Ord α] (input : Mappings α β) : Mappings2 α β := let input := input.mappings.quicksortBy λ a b ↦ (Ord.compare a.inputStart b.inputStart == Ordering.lt) let rec helper := λ | [] => [] | a :: [] => [{ start:= a.inputStart, offset := (NatId.toNat a.outputStart) - (NatId.toNat a.inputStart)}, {start:= NatId.fromNat (NatId.toNat a.inputStart + a.length), offset := 0}] | a :: b :: as => if (NatId.toNat b.inputStart) != (NatId.toNat a.inputStart + a.length) then { start:= a.inputStart, offset := (NatId.toNat a.outputStart) - (NatId.toNat a.inputStart)} :: { start:= NatId.fromNat (NatId.toNat a.inputStart + a.length), offset := 0} :: helper (b :: as) else { start:= a.inputStart, offset := (NatId.toNat a.outputStart) - (NatId.toNat a.inputStart)} :: helper (b :: as) let result := match input with | [] => [] | a :: _ => if NatId.toNat a.inputStart != 0 then { start:= NatId.fromNat 0, offset := 0 : Mapping2 α β} :: helper input else helper input Mappings2.mk result private def Mappings2.apply (α β : Type) [NatId α] [NatId β] [Ord α] (mapping : Mappings2 α β) (value : α) : β := let rec findOffsetHelper := λ | [] => 0 | a :: [] => a.offset | a :: b :: as => if (Ord.compare value b.start == Ordering.lt) then a.offset else findOffsetHelper (b :: as) let offset : Int := findOffsetHelper mapping.mappings let result : Int := (NatId.toNat value + offset) NatId.fromNat result.toNat private def Mappings2.combine {α β γ : Type} [NatId α] [NatId β] [NatId γ] (a : Mappings2 α β) (b : Mappings2 β γ) : Mappings2 α γ := -- at this point, let's just go integer let a : List (Int × Int) := a.mappings.map λ m ↦ (NatId.toNat m.start, m.offset) let b : List (Int × Int):= b.mappings.map λ m ↦ (NatId.toNat m.start, m.offset) let rec findOffsetHelper := λ (list : List (Int × Int)) (value : Int) ↦ match list with | [] => 0 | a :: [] => a.snd | a :: b :: as => if (value < b.fst) then a.snd else findOffsetHelper (b :: as) value let rec helper := λ | [] => b | a :: [] => let bOffsetAtA := findOffsetHelper b (a.fst + a.snd) let bRemainder := b.dropWhile (λ (bb : Int × Int) ↦ a.fst + a.snd > bb.fst) match bRemainder with | [] => [(a.fst, a.snd + bOffsetAtA)] | b :: _ => if b.fst - a.snd == a.fst then bRemainder.map λ (b : Int × Int) ↦ (b.fst - a.snd, a.snd + b.snd) else (a.fst, a.snd + bOffsetAtA) :: bRemainder.map λ (b : Int × Int) ↦ (b.fst - a.snd, a.snd + b.snd) | a :: aa :: as => let bOffsetAtA := findOffsetHelper b (a.fst + a.snd) let relevantBs := b.filter (λ (bb : Int × Int) ↦ a.fst + a.snd ≤ bb.fst && aa.fst + a.snd > bb.fst) match relevantBs with | [] => (a.fst, a.snd + bOffsetAtA) :: (helper (aa :: as)) | b :: _ => if b.fst - a.snd == a.fst then (relevantBs.map λ (b : Int × Int) ↦ (b.fst - a.snd, a.snd + b.snd)) ++ helper (aa :: as) else (a.fst, a.snd + bOffsetAtA) :: (relevantBs.map λ (b : Int × Int) ↦ (b.fst - a.snd, a.snd + b.snd)) ++ helper (aa :: as) let result := helper a Mappings2.mk $ result.map λ p ↦ { start := NatId.fromNat p.fst.toNat, offset := p.snd : Mapping2 α γ} private structure SeedRange where start : Seed ending : Seed deriving Repr private def SeedRange.fromList (input : List Seed) : List SeedRange := let rec helper : List Seed → List SeedRange := λ | [] => [] | _ :: [] => [] | a :: b :: as => { start := a, ending := Seed.mk $ b.id + a.id} :: SeedRange.fromList as (helper input).quicksortBy λ a b ↦ a.start.id < b.start.id private def SeedRange.findSmallestSeedAbove (seedRanges : List SeedRange) (value : Seed) : Option Seed := -- two options: If the value is inside a seedRange, the value itself is the result -- If not, the start of the first seedRange above the value is the result let rangeContains := λ r ↦ (Ord.compare r.start value != Ordering.gt) && (Ord.compare r.ending value == Ordering.gt) let rec helper := λ | [] => none | r :: rs => if rangeContains r then some value else if Ord.compare r.start value == Ordering.gt then r.start else helper rs helper seedRanges def part2 (input : ((List Seed) × Almanach)) : Option Nat := let a := input.snd let seedToLocation := Mappings2.fromMappings a.seedsToSoil |> flip Mappings2.combine (Mappings2.fromMappings a.soilToFertilizer) |> flip Mappings2.combine (Mappings2.fromMappings a.fertilizerToWater) |> flip Mappings2.combine (Mappings2.fromMappings a.waterToLight) |> flip Mappings2.combine (Mappings2.fromMappings a.lightToTemperature) |> flip Mappings2.combine (Mappings2.fromMappings a.temperatureToHumidity) |> flip Mappings2.combine (Mappings2.fromMappings a.humidityToLocation) let seedRanges := SeedRange.fromList input.fst let potentialSeeds := seedToLocation.mappings.filterMap λ m ↦ (SeedRange.findSmallestSeedAbove seedRanges m.start) -- could filter by range end, but who cares? let locations := potentialSeeds.map seedToLocation.apply NatId.toNat <$> locations.minimum?
[C]
My first approach to part 2 was to take the list of ranges, map it to a new list of (possibly split up) ranges, etc, but I realized that would take more memory and bookkeeping than I'd like. Scrapped it and rewrote it with recursion. Much cleaner and the benchmarks are still looking good! (But for how much longer?)
$ bmake bench day01 0:00.00 1380 Kb 0+78 faults day02 0:00.00 1660 Kb 0+82 faults day03 0:00.00 1540 Kb 0+107 faults day04 0:00.00 1536 Kb 0+80 faults day05 0:00.00 1668 Kb 0+83 faults
https://github.com/sjmulder/aoc/blob/master/2023/c/day05.c
Edit: I see some people went for looping from 0 to try possible answers. Clever and short but I wanted to go for a more efficient approach.
Ah, nice! Dealing with each range individually makes things much simpler.
Well, I can't say much about this one. The code is ugly, horribly inefficient, and part two takes a solid half hour to run. It got the right answer though, so that's something I suppose. I think something like
nom
to parse the input would be much cleaner, and there's got to be a better way of going about part two than just brute forcing through every possible seed, but hey, it works so that's good enough for now.https://github.com/capitalpb/advent_of_code_2023/blob/main/src/solvers/day05.rs
#[derive(Clone, Debug)] struct AlmanacMapEntry { destination_range: RangeInclusive, source_range: RangeInclusive, } #[derive(Clone, Debug)] struct AlmanacMap { entries: Vec, } impl AlmanacMap { fn from(input: &str) -> AlmanacMap { let entries = input .lines() .skip(1) .map(|line| { let numbers = line .split(' ') .filter_map(|number| number.parse::().ok()) .collect::>(); AlmanacMapEntry { destination_range: numbers[0]..=(numbers[0] + numbers[2]), source_range: numbers[1]..=(numbers[1] + numbers[2]), } }) .collect(); AlmanacMap { entries } } fn convert(&self, source: &u64) -> u64 { let entry = self .entries .iter() .find(|entry| entry.source_range.contains(&source)); if let Some(entry) = entry { entry.destination_range.start() + (source - entry.source_range.start()) } else { source.clone() } } } #[derive(Debug)] struct Almanac { seeds: Vec, seed_to_soil: AlmanacMap, soil_to_fertilizer: AlmanacMap, fertilizer_to_water: AlmanacMap, water_to_light: AlmanacMap, light_to_temperature: AlmanacMap, temperature_to_humidity: AlmanacMap, humidity_to_location: AlmanacMap, } impl Almanac { fn star_one_from(input: &str) -> Almanac { let mut input_sections = input .split("\n\n") .map(|section| section.split_once(':').unwrap().1); let seeds = input_sections .next() .unwrap() .split_whitespace() .filter_map(|seed| seed.parse::().ok()) .collect(); let almanac_maps = input_sections.map(AlmanacMap::from).collect::>(); Almanac { seeds, seed_to_soil: almanac_maps[0].clone(), soil_to_fertilizer: almanac_maps[1].clone(), fertilizer_to_water: almanac_maps[2].clone(), water_to_light: almanac_maps[3].clone(), light_to_temperature: almanac_maps[4].clone(), temperature_to_humidity: almanac_maps[5].clone(), humidity_to_location: almanac_maps[6].clone(), } } fn star_two_from(input: &str) -> Almanac { let mut input_sections = input .split("\n\n") .map(|section| section.split_once(':').unwrap().1); let seeds = input_sections .next() .unwrap() .split_whitespace() .filter_map(|seed| seed.parse::().ok()) .collect::>() .chunks(2) .map(|chunk| (chunk[0]..(chunk[0] + chunk[1])).collect::>()) .flatten() .collect::>(); let almanac_maps = input_sections.map(AlmanacMap::from).collect::>(); Almanac { seeds, seed_to_soil: almanac_maps[0].clone(), soil_to_fertilizer: almanac_maps[1].clone(), fertilizer_to_water: almanac_maps[2].clone(), water_to_light: almanac_maps[3].clone(), light_to_temperature: almanac_maps[4].clone(), temperature_to_humidity: almanac_maps[5].clone(), humidity_to_location: almanac_maps[6].clone(), } } } pub struct Day05; impl Solver for Day05 { fn star_one(&self, input: &str) -> String { let almanac = Almanac::star_one_from(input); almanac .seeds .iter() .map(|seed| almanac.seed_to_soil.convert(seed)) .map(|soil| almanac.soil_to_fertilizer.convert(&soil)) .map(|fertilizer| almanac.fertilizer_to_water.convert(&fertilizer)) .map(|water| almanac.water_to_light.convert(&water)) .map(|light| almanac.light_to_temperature.convert(&light)) .map(|temperature| almanac.temperature_to_humidity.convert(&temperature)) .map(|humidity| almanac.humidity_to_location.convert(&humidity)) .min() .unwrap() .to_string() } fn star_two(&self, input: &str) -> String { let almanac = Almanac::star_two_from(input); almanac .seeds .iter() .map(|seed| almanac.seed_to_soil.convert(seed)) .map(|soil| almanac.soil_to_fertilizer.convert(&soil)) .map(|fertilizer| almanac.fertilizer_to_water.convert(&fertilizer)) .map(|water| almanac.water_to_light.convert(&water)) .map(|light| almanac.light_to_temperature.convert(&light)) .map(|temperature| almanac.temperature_to_humidity.convert(&temperature)) .map(|humidity| almanac.humidity_to_location.convert(&humidity)) .min() .unwrap() .to_string() } }
Scala3
kind of convoluted, but purely functional
import scala.collection.immutable.NumericRange.Exclusive import math.max import math.min extension [A] (l: List[A]) def chunk(pred: A => Boolean): List[List[A]] = def go(l: List[A], partial_acc: List[A], acc: List[List[A]]): List[List[A]] = l match case (h :: t) if pred(h) => go(t, List(), partial_acc.reverse :: acc) case (h :: t) => go(t, h :: partial_acc, acc) case _ => partial_acc.reverse :: acc go(l, List(), List()).reverse type R = Exclusive[Long] def intersectTranslate(r: R, c: R, t: Long): R = (t + max(r.start, c.start) - c.start) until (t + min(r.end, c.end) - c.start) case class MappingEntry(from: R, to: Long) case class Mapping(entries: List[MappingEntry], produces: String): def resolveRange(in: R): List[R] = entries.map(e => intersectTranslate(in, e.from, e.to)).filter(!_.isEmpty) def completeEntries(a: List[MappingEntry]): List[MappingEntry] = a ++ ((0L until 0L) +: a.map(_.from).sorted :+ (Long.MaxValue until Long.MaxValue)).sliding(2).flatMap{ case List(a, b) => Some(MappingEntry(a.end until b.start, a.end)); case _ => None}.toList def parse(a: List[String], init: List[Long] => List[R]): (List[R], Map[String, Mapping]) = def parseEntry(s: String): MappingEntry = s match case s"$end $start $range" => MappingEntry(start.toLong until start.toLong + range.toLong, end.toLong) a.chunk(_ == "") match case List(s"seeds: $seeds") :: maps => init(seeds.split(raw"\s+").map(_.toLong).toList) -> (maps.flatMap{ case s"$from-to-$to map:" :: entries => Some(from -> Mapping(completeEntries(entries.map(parseEntry)), to)); case _ => None }).toMap case _ => (List(), Map()).ensuring(false) def singletons(a: List[Long]): List[R] = a.map(s => s until s + 1) def paired(a: List[Long]): List[R] = a.grouped(2).flatMap{ case List(x, y) => Some(x until x+y); case _ => None }.toList def chase(d: (List[R], Map[String, Mapping]), initial: String, target: String) = val (init, m) = d def go(a: List[R], s: String): List[R] = if trace(s) == target then a else val x = m(s) go(a.flatMap(x.resolveRange), x.produces) go(trace(init), initial) def task1(a: List[String]): Long = chase(parse(a, singletons), "seed", "location").min.start def task2(a: List[String]): Long = chase(parse(a, paired), "seed", "location").min.start
This was interesting! So iterating through the solution space would be infeasible here and it seems we need to look for boundaries between regions and follow them to find places where a solution could occur.
Python: https://pastebin.com/8Ckx36fu
- Make a list of places where location mappings are discontinuous (0, the end of each mapping, and the number before)
- Repeat this for discontinuities in each intermediate layer
- Trace each such location to its seed, and filter by seed ranges
- Run the very minimal set of interesting seed numbers (around 2000 seeds) through the existing part1 algorithm
Python
Questions and feedback welcome!
import portion as P from .solver import Solver _maps = [ 'seed-to-soil', 'soil-to-fertilizer', 'fertilizer-to-water', 'water-to-light', 'light-to-temperature', 'temperature-to-humidity', 'humidity-to-location', ] def group_lines_in_maps(lines): group = [] for line in lines: if not line: yield group group = [] continue group.append(line) yield group class Day05(Solver): def __init__(self): super().__init__(5) self.seeds = [] self.mappings = {} def presolve(self, input: str): lines = input.rstrip().split('\n') self.seeds = list(map(int, lines[0].split(' ')[1:])) self.mappings = {} for mapping in group_lines_in_maps(lines[2:]): mapping_name = mapping[0].split(' ')[0] mapping_ranges = map(lambda rng: tuple(map(int, rng.split(' '))), mapping[1:]) self.mappings[mapping_name] = list(mapping_ranges) def solve_first_star(self): locations = [] for seed in self.seeds: location = seed for mapping in map(self.mappings.get, _maps): assert mapping for dest, source, length in mapping: if 0 <= location - source < length: location = dest + (location - source) break locations.append(location) return min(locations) def solve_second_star(self): current_set = P.empty() for i in range(0, len(self.seeds), 2): current_set = current_set | P.closedopen(self.seeds[i], self.seeds[i] + self.seeds[i + 1]) for mapping in map(self.mappings.get, _maps): assert mapping unmapped = current_set next_set = P.empty() for dest, source, length in mapping: delta = dest - source source_range = P.closedopen(source, source + length) mappable = unmapped & source_range mapped_to_destination = mappable.apply( lambda x: (x.left, x.lower + delta, x.upper + delta, x.right)) # pylint: disable=cell-var-from-loop next_set = next_set | mapped_to_destination unmapped = unmapped - source_range current_set = next_set | unmapped return next(P.iterate(current_set, step=1))
Honestly this one was quite easy for me. Not so much for my computer though...
https://git.sr.ht/~aidenisik/aoc23/tree/master/item/day5
C solutions. Disclaimer, part 2 has not finished running. But it's mostly the same code as part 1 and it works on the small sample data so it'll be fine.