diff --git a/advent-of-code/2023/day_05/.gitignore b/advent-of-code/2023/day_05/.gitignore new file mode 100644 index 0000000..69843e7 --- /dev/null +++ b/advent-of-code/2023/day_05/.gitignore @@ -0,0 +1,42 @@ +.gradle +gradle* +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Kotlin ### +.kotlin + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/advent-of-code/2023/day_05/README.md b/advent-of-code/2023/day_05/README.md new file mode 100644 index 0000000..9eb7a2b --- /dev/null +++ b/advent-of-code/2023/day_05/README.md @@ -0,0 +1,161 @@ +# Advent of Code 2023 day 5 solution in Kotlin + +## If You Give A Seed A Fertilizer + +[Task page](https://adventofcode.com/2023/day/5) + +You take the boat and find the gardener right where you were told he would be: managing a giant "garden" +that looks more to you like a farm. + +"A water source? Island Island is the water source!" You point out that Snow Island isn't receiving any water. + +"Oh, we had to stop the water because we ran out of sand to filter it with! Can't make snow with dirty water. +Don't worry, I'm sure we'll get more sand soon; we only turned off the water a few days... weeks... oh no." +His face sinks into a look of horrified realization. + +"I've been so busy making sure everyone here has food that I completely forgot to check why we stopped +getting more sand! There's a ferry leaving soon that is headed over in that direction - it's much faster +than your boat. Could you please go check it out?" + +You barely have time to agree to this request when he brings up another. "While you wait for the ferry, +maybe you can help us with our food production problem. The latest Island Island Almanac just arrived and +we're having trouble making sense of it." + +The almanac (your puzzle input) lists all of the seeds that need to be planted. +It also lists what type of soil to use with each kind of seed, what type of fertilizer to use with each kind of soil, +what type of water to use with each kind of fertilizer, and so on. Every type of seed, soil, fertilizer +and so on is identified with a number, but numbers are reused by each category - that is, +soil 123 and fertilizer 123 aren't necessarily related to each other. + +For example: + +``` +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 +``` + +The almanac starts by listing which seeds need to be planted: seeds 79, 14, 55, and 13. + +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. + +Consider again the example seed-to-soil map: + +``` +50 98 2 +52 50 48 +``` + +The first line has a destination range start of 50, a source range start of 98, and a range length of 2. +This line means that the source range starts at 98 and contains two values: 98 and 99. +The destination range is the same length, but it starts at 50, so its two values are 50 and 51. +With this information, you know that seed number 98 corresponds to soil number 50 and that seed number 99 corresponds +to soil number 51. + +The second line means that the source range starts at 50 and contains 48 values: 50, 51, ..., 96, 97. +This corresponds to a destination range starting at 52 and also containing 48 values: 52, 53, ..., 98, 99. +So, seed number 53 corresponds to soil number 55. + +Any source numbers that aren't mapped correspond to the same destination number. +So, seed number 10 corresponds to soil number 10. + +So, the entire list of seed numbers and their corresponding soil numbers looks like this: + +``` +seed soil +0 0 +1 1 +... ... +48 48 +49 49 +50 52 +51 53 +... ... +96 98 +97 99 +98 50 +99 51 +``` + +With this map, you can look up the soil number required for each initial seed number: + +- Seed number 79 corresponds to soil number 81. +- Seed number 14 corresponds to soil number 14. +- Seed number 55 corresponds to soil number 57. +- Seed number 13 corresponds to soil number 13. + +The gardener and his team want to get started as soon as possible, so they'd like to know the closest location that +needs a seed. Using these maps, find the lowest location number that corresponds to any of the initial seeds. +To do this, you'll need to convert each seed number through other categories until you can find its corresponding +location number. In this example, the corresponding types are: + +- Seed 79, soil 81, fertilizer 81, water 81, light 74, temperature 78, humidity 78, location 82. +- Seed 14, soil 14, fertilizer 53, water 49, light 42, temperature 42, humidity 43, location 43. +- Seed 55, soil 57, fertilizer 57, water 53, light 46, temperature 82, humidity 82, location 86. +- Seed 13, soil 13, fertilizer 52, water 41, light 34, temperature 34, humidity 35, location 35. + +So, the lowest location number in this example is 35. + +What is the lowest location number that corresponds to any of the initial seed numbers? + + +## Part Two + +Everyone will starve if you only plant such a small number of seeds. +Re-reading the almanac, it looks like the seeds: line actually describes ranges of seed numbers. + +The values on the initial seeds: line come in pairs. Within each pair, the first value is the start of the range +and the second value is the length of the range. So, in the first line of the example above: + +``` +seeds: 79 14 55 13 +``` + +This line describes two ranges of seed numbers to be planted in the garden. +The first range starts with seed number 79 and contains 14 values: 79, 80, ..., 91, 92. +The second range starts with seed number 55 and contains 13 values: 55, 56, ..., 66, 67. + +Now, rather than considering four seed numbers, you need to consider a total of 27 seed numbers. + +In the above example, the lowest location number can be obtained from seed number 82, which corresponds to soil 84, +fertilizer 84, water 84, light 77, temperature 45, humidity 46, and location 46. So, the lowest location number is 46. + +Consider all of the initial seed numbers listed in the ranges on the first line of the almanac. +What is the lowest location number that corresponds to any of the initial seed numbers? diff --git a/advent-of-code/2023/day_05/build.gradle.kts b/advent-of-code/2023/day_05/build.gradle.kts new file mode 100644 index 0000000..6397ac9 --- /dev/null +++ b/advent-of-code/2023/day_05/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + kotlin("jvm") version "2.0.21" +} + +group = "space.comfycamp" +version = "1.0" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(kotlin("test")) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/advent-of-code/2023/day_05/settings.gradle.kts b/advent-of-code/2023/day_05/settings.gradle.kts new file mode 100644 index 0000000..99684fc --- /dev/null +++ b/advent-of-code/2023/day_05/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "day_05" + diff --git a/advent-of-code/2023/day_05/src/main/kotlin/Almanac.kt b/advent-of-code/2023/day_05/src/main/kotlin/Almanac.kt new file mode 100644 index 0000000..8be300b --- /dev/null +++ b/advent-of-code/2023/day_05/src/main/kotlin/Almanac.kt @@ -0,0 +1,131 @@ +package space.comfycamp + +import kotlin.math.min +import kotlin.math.max + +// Part 1. +fun getLowestLocationNumber(lines: List): Long { + val almanac = parseAlmanac(lines) + return almanac.seeds + .map { + var num = it + + for (map in almanac.maps) { + num = mapSrcNumberToDest(map, num) + } + + num + } + .min() +} + +// Part 2. +fun getLowestLocationNumberForRanges(lines: List): Long { + val almanac = parseAlmanac(lines) + var ranges = mutableListOf() + + for (i in almanac.seeds.indices step 2) { + ranges.add(LongRange(almanac.seeds[i], almanac.seeds[i] + almanac.seeds[i + 1] - 1)) + } + + for (map in almanac.maps) { + var minCheckedValue = Long.MIN_VALUE + val newList = mutableListOf() + + for (mapper in map.sortedBy { it.sourceRangeStart }) { + val mapperEnd = mapper.sourceRangeStart + mapper.rangeLength - 1 + val diff = mapper.destRangeStart - mapper.sourceRangeStart + + // find all values before this range. + val unmatchedValues = ranges + .map { + LongRange( + max(minCheckedValue, it.first), + min(mapper.sourceRangeStart - 1, it.last), + ) + } + .filter { it.first <= it.last } + newList.addAll(unmatchedValues) + + // map values + newList.addAll( + ranges + .map { + LongRange( + max(mapper.sourceRangeStart, it.first) + diff, + min(mapperEnd, it.last) + diff, + ) + } + .filter { it.first <= it.last } + ) + + minCheckedValue = mapperEnd + 1 + } + + newList.addAll( + ranges + .filter { it.last >= minCheckedValue } + .map { + LongRange( + max(it.first, minCheckedValue), + it.last, + ) + } + ) + + ranges = newList + } + + return ranges + .map { it.first } + .min() +} + +data class Almanac(val seeds: MutableList, val maps: MutableList>) + +private fun parseAlmanac(lines: List): Almanac { + val seeds = lines[0] + .split(": ")[1] + .split(" ") + .map { it.toLong() } + .toMutableList() + + var maps = mutableListOf>() + + for (line in lines.subList(2, lines.size)) { + if (line.isEmpty()) continue + + if (line.contains(" map:")) { + maps.add(mutableListOf()) + continue + } + + val (destRangeStart, srcRangeStart, rangeLength) = line.split(" ").map{ it.toLong() } + maps.last().add( + Mapper(srcRangeStart, destRangeStart, rangeLength) + ) + } + + return Almanac(seeds, maps) +} + +fun mapSrcNumberToDest(mappers: List, src: Long): Long { + for (range in mappers) { + if (range.isNumberInSrcRange(src)) { + return range.srcValueToDest(src) + } + } + + return src +} + +// Mapper contains one line of map. +data class Mapper(val sourceRangeStart: Long, val destRangeStart: Long, val rangeLength: Long) { + fun srcValueToDest(v: Long): Long { + return destRangeStart + v - sourceRangeStart + } + + fun isNumberInSrcRange(num: Long): Boolean { + return num >= sourceRangeStart && num < sourceRangeStart + rangeLength + } +} diff --git a/advent-of-code/2023/day_05/src/main/kotlin/Main.kt b/advent-of-code/2023/day_05/src/main/kotlin/Main.kt new file mode 100644 index 0000000..803ae23 --- /dev/null +++ b/advent-of-code/2023/day_05/src/main/kotlin/Main.kt @@ -0,0 +1,12 @@ +package space.comfycamp + +fun main() { + val resource = object{}.javaClass.getResource("/input.txt")!! + val lines = resource.readText().trim().lines() + + val res1 = getLowestLocationNumber(lines) + println("Part 1: $res1") + + val res2 = getLowestLocationNumberForRanges(lines) + println("Part 2: $res2") +} diff --git a/advent-of-code/2023/kotlin/src/main/resources/day-05/input.txt b/advent-of-code/2023/day_05/src/main/resources/input.txt similarity index 100% rename from advent-of-code/2023/kotlin/src/main/resources/day-05/input.txt rename to advent-of-code/2023/day_05/src/main/resources/input.txt diff --git a/advent-of-code/2023/day_05/src/test/kotlin/AlmanacTest.kt b/advent-of-code/2023/day_05/src/test/kotlin/AlmanacTest.kt new file mode 100644 index 0000000..55a42c5 --- /dev/null +++ b/advent-of-code/2023/day_05/src/test/kotlin/AlmanacTest.kt @@ -0,0 +1,18 @@ +import kotlin.test.assertEquals +import kotlin.test.Test +import space.comfycamp.getLowestLocationNumber +import space.comfycamp.getLowestLocationNumberForRanges + +class AlmanacTest { + val lines = object{}.javaClass.getResource("/input.txt")!!.readText().trim().lines() + + @Test + fun testGetLowestLocationNumber() { + assertEquals(35, getLowestLocationNumber(lines)) + } + + @Test + fun testGetLowestLocationNumberForRanges() { + assertEquals(46, getLowestLocationNumberForRanges(lines)) + } +} diff --git a/advent-of-code/2023/kotlin/src/main/resources/day-05/test.txt b/advent-of-code/2023/day_05/src/test/resources/input.txt similarity index 100% rename from advent-of-code/2023/kotlin/src/main/resources/day-05/test.txt rename to advent-of-code/2023/day_05/src/test/resources/input.txt diff --git a/advent-of-code/2023/kotlin/src/main/kotlin/Solution05.kt b/advent-of-code/2023/kotlin/src/main/kotlin/Solution05.kt deleted file mode 100644 index 6d069f1..0000000 --- a/advent-of-code/2023/kotlin/src/main/kotlin/Solution05.kt +++ /dev/null @@ -1,138 +0,0 @@ -import kotlin.math.min -import kotlin.math.max - -class Solution05(lines: List) { - private var seeds = mutableListOf() - private var maps = mutableListOf() - - init { - // Parse seeds - val (_, seedsStr) = lines[0].split(": ") - seeds.addAll(seedsStr.split(" ").map{it.toLong()}) - - // Parse all maps - for (line in lines.subList(2, lines.size)) { - if (line.isEmpty()) continue - - if (line.contains(" map:")) { - maps.add(MapList()) - continue - } - - val (destRangeStart, srcRangeStart, rangeLength) = line.split(" ") - maps.last().addRange( - srcRangeStart.toLong(), - destRangeStart.toLong(), - rangeLength.toLong(), - ) - } - } - - // Part 1. - fun getLowestLocationNumber(): Long { - return seeds - .map{ - var num = it - - for (map in maps) { - num = map.mapSrcNumberToDest(num) - } - - num - } - .min() - } - - // Part 2. - fun getLowestLocationNumberForRangeOfSeeds(): Long { - var ranges = mutableListOf() - - for (i in seeds.indices step 2) { - ranges.add(LongRange(seeds[i], seeds[i] + seeds[i+1] - 1)) - } - - for (map in maps) { - var minCheckedValue = Long.MIN_VALUE - val newList = mutableListOf() - - for (mapper in map.mappers.sortedBy{it.sourceRangeStart}) { - val mapperEnd = mapper.sourceRangeStart + mapper.rangeLength - 1 - val diff = mapper.destRangeStart - mapper.sourceRangeStart - - // find all values before this range. - val unmatchedValues = ranges - .map{ - LongRange( - max(minCheckedValue, it.first), - min(mapper.sourceRangeStart - 1, it.last), - ) - } - .filter{it.first <= it.last} - newList.addAll(unmatchedValues) - - // map values - newList.addAll( - ranges - .map{ - LongRange( - max(mapper.sourceRangeStart, it.first) + diff, - min(mapperEnd, it.last) + diff, - ) - } - .filter{it.first <= it.last} - ) - - minCheckedValue = mapperEnd + 1 - } - - newList.addAll( - ranges - .filter{it.last >= minCheckedValue} - .map{ - LongRange( - max(it.first, minCheckedValue), - it.last, - ) - } - ) - - ranges = newList - } - - return ranges - .map{it.first} - .min() - } -} - -// Map stores the rules for matching one number to another. -class MapList() { - val mappers = mutableListOf() - - fun addRange(sourceRangeStart: Long, destRangeStart: Long, rangeLength: Long) { - mappers.add( - Mapper(sourceRangeStart, destRangeStart, rangeLength), - ) - } - - fun mapSrcNumberToDest(src: Long): Long { - for (range in mappers) { - if (range.isNumberInSrcRange(src)) { - return range.srcValueToDest(src) - } - } - - return src - } -} - -// Mapper contains one line of map. -data class Mapper(val sourceRangeStart: Long, val destRangeStart: Long, val rangeLength: Long) { - fun srcValueToDest(v: Long): Long { - return destRangeStart + v - sourceRangeStart - } - - fun isNumberInSrcRange(num: Long): Boolean { - return num >= sourceRangeStart && num < sourceRangeStart + rangeLength - } -} diff --git a/advent-of-code/2023/kotlin/src/test/kotlin/Solution05Test.kt b/advent-of-code/2023/kotlin/src/test/kotlin/Solution05Test.kt deleted file mode 100644 index 6a4edbd..0000000 --- a/advent-of-code/2023/kotlin/src/test/kotlin/Solution05Test.kt +++ /dev/null @@ -1,39 +0,0 @@ -import kotlin.test.Test -import kotlin.test.assertEquals - -class Solution05Test { - @Test - fun testGetLowestLocation() { - val text = ResourceReader().readFile("day-05/test.txt") - val res = Solution05(text).getLowestLocationNumber() - assertEquals(35L, res) - } - - @Test - fun solvePart1() { - val text = ResourceReader().readFile("day-05/input.txt") - val res = Solution05(text).getLowestLocationNumber() - assertEquals(621354867L, res) - } - - @Test - fun testGetLowestLocationForRangeOfSeeds() { - val text = ResourceReader().readFile("day-05/test.txt") - val res = Solution05(text).getLowestLocationNumberForRangeOfSeeds() - assertEquals(46, res) - } - - @Test - fun solvePart2() { - val text = ResourceReader().readFile("day-05/input.txt") - val res = Solution05(text).getLowestLocationNumberForRangeOfSeeds() - println("Your answer: $res") - } - - @Test - fun testPart2() { - val text = ResourceReader().readFile("day-05/input.txt") - val res = Solution05(text).getLowestLocationNumberForRangeOfSeeds() - assertEquals(15880236, res) - } -}