import java.io.File typealias Map = Pair fun main() { val input = File("input") val (seeds, maps) = parseInput(input) println(part1(seeds, maps)) val seedRanges = seeds.windowed(2, 2) { it.first() ..< it.first() + it.last() } println(part2(seedRanges, maps)) } fun part1(seeds: List, maps: List>): UInt { return seeds.map { seed -> maps.fold(seed) { toFind, map -> findMapping(toFind, map) } }.min() } fun findMapping(toFind: UInt, maps: List): UInt { for (map in maps) { if (toFind >= map.first.first && toFind <= map.first.last) { val index = toFind - map.first.first if (map.second.first + index <= map.second.last) { return map.second.first + index } } } // Any source numbers that aren't mapped correspond to the same // destination number. So, seed number 10 corresponds to soil // number 10. return toFind } fun part2(seedRanges: List, maps: List>): UInt { return seedRanges .map { range -> maps .fold(listOf(range)) { toReduce, map -> findReduction(toReduce, map) } .filter { // TODO(tlater): Remove this cursed filter // // Sometimes the source and range match up // perfectly, and the destination starts // with 0. // // Maybe this is a bug in the puzzle (?!), // or whenever this happens my logic // breaks down and the resulting // translated range always ends up in the // result. // // Nonetheless, filtering these 0s out // gives the right answer. // // More likely it's a booby trap for // *exactly* the logical fallacy I'm // committing? // // That, *or* it's just unlikely any one // range is the lowest in the end, and // these don't happen to contribute, while // the rest is computed correctly. it.first != 0u } .map { it.first } .min() } .min() } fun findReduction(toReduce: List, maps: List): List { var result: MutableList = mutableListOf() var reducing: MutableList = mutableListOf() reducing.addAll(toReduce) for (map in maps) { var next: MutableList = mutableListOf() val source = map.first val destination = map.second for (currentRange in reducing) { val (overlap, excess) = getOverlap(currentRange, source) if (overlap != null) { result.add(getDestinationForOverlap(overlap, source, destination)) } next.addAll(excess) } reducing = next } result.addAll(reducing) return result } fun getDestinationForOverlap( overlap: UIntRange, source: UIntRange, destination: UIntRange ): UIntRange { val startDiff = overlap.first - source.first val endDiff = source.last - overlap.last return destination.first + startDiff..destination.last - endDiff } fun getOverlap(range1: UIntRange, range2: UIntRange): Pair> { val start = when { range1.first <= range2.first -> range2.first range1.first > range2.first -> range1.first else -> throw Exception("Unreachable") } val end = when { range1.last >= range2.last -> range2.last range1.last < range2.last -> range1.last else -> throw Exception("Unreachable") } if (start > end) { return Pair(null, listOf(range1)) } else { val excess: MutableList = mutableListOf() if (start > range1.first) { excess.add(range1.first ..< start) } if (range1.last > end) { excess.add(end + 1u..range1.last) } return Pair(start..end, excess) } } fun parseInput(input: File): Pair, List>> { val text = input.bufferedReader() val seeds = text.readLine().split(": ")[1].split(" ").map { it.toUInt() } text.readLine() var line = text.readLine() var maps: MutableList> = mutableListOf() while (line != null) { when { line == "" -> Unit // Skip empty lines line.endsWith(':') -> maps.add(mutableListOf()) else -> { val (destination, source, length) = line.split(" ").map { it.toUInt() } maps.last() .add(Pair(source ..< source + length, destination ..< destination + length)) } } line = text.readLine() } return Pair(seeds, maps) }