Refactor day 5 of AoC 2023

This commit is contained in:
Ivan R. 2024-12-21 03:49:19 +05:00
parent e87578a619
commit 3a04689db8
Signed by: lumin
GPG key ID: E0937DC7CD6D3817
11 changed files with 384 additions and 177 deletions

42
advent-of-code/2023/day_05/.gitignore vendored Normal file
View file

@ -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

View file

@ -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?

View file

@ -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()
}

View file

@ -0,0 +1,2 @@
rootProject.name = "day_05"

View file

@ -0,0 +1,131 @@
package space.comfycamp
import kotlin.math.min
import kotlin.math.max
// Part 1.
fun getLowestLocationNumber(lines: List<String>): 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<String>): Long {
val almanac = parseAlmanac(lines)
var ranges = mutableListOf<LongRange>()
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<LongRange>()
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<Long>, val maps: MutableList<MutableList<Mapper>>)
private fun parseAlmanac(lines: List<String>): Almanac {
val seeds = lines[0]
.split(": ")[1]
.split(" ")
.map { it.toLong() }
.toMutableList()
var maps = mutableListOf<MutableList<Mapper>>()
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<Mapper>, 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
}
}

View file

@ -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")
}

View file

@ -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))
}
}

View file

@ -1,138 +0,0 @@
import kotlin.math.min
import kotlin.math.max
class Solution05(lines: List<String>) {
private var seeds = mutableListOf<Long>()
private var maps = mutableListOf<MapList>()
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<LongRange>()
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<LongRange>()
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<Mapper>()
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
}
}

View file

@ -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)
}
}