Advent of Code 2022 - Day 20, in Kotlin - Grove Positioning System
Kotlin solutions to parts 1 and 2 of Advent of Code 2022, Day 20: 'Grove Positioning System'
When I first read today’s puzzle I thought it would be really easy. It turns out that the sample data omits a condition that our real data has, and that threw me off for a good amount of time. It’s important to not make assumptions about your input!
If you’d rather just view code, the GitHub Repository is here .
Puzzle Input
We won’t do any special parsing with our input
today. Since we’ll have to do this for each part of the puzzle, we’ll declare it as a List<String>
property in the constructor and move on.
class Day20(private val input: List<String>) {
}
⭐ Day 20, Part 1
The puzzle text can be found here.
Before we get too far into the solution, let me tell you what messed me up for a non-trivial amount of time. My list had duplicates and that means I can’t always say “give me the index of the first 5” because there might be more than one 5, and they might have been shifted around so that the first 5 is really the second 5.
So we need not only keep track of the numbers to move in order, but their precise position within the original and target lists. One way to do this is to use a data class (or a Pair
, if you want) to store the originalIndex
of the number from the input
and the value
itself. When we look up a number, we find it by its originalIndex
. That way, we know the 5 we get is the correct 5, not some shifty imposter 5.
We’ll call our tracking-class MappedNumber
.
// In Day20
private data class MappedNumber(val originalIndex: Int, val value: Long)
Why is value
a Long
and not anInt
? Because in Part Two we deal with large numbers. Let’s parse these with parseInput
using mapIndexed
, and return the result as a MutableList<MappedNumber>
.
// In Day20
private fun parseInput(L): MutableList<MappedNumber> =
input.mapIndexed { index, value ->
MappedNumber(index, value.toLong())
}.toMutableList()
Before we get to the decryption function, let’s write the groveCoordinates
function we’ll need to give us the answer.
// In Day20
private fun List<MappedNumber>.groveCoordinates(): Long {
val zero = indexOfFirst { it.value == 0L }
return listOf(1000, 2000, 3000).sumOf { this[(zero + it) % size].value }
}
As you can see, we can look up the location of zero directly - my input only had one of them. Maybe intentionally? We create a list of 1000, 2000, and 3000, and add the index of the zero
to it, and modulus the size of the list to account for the fact that the list is circular. This gives us the MappedNumber
objects we’re looking for, so we can take the sumOf
their value
.
On to decryption!
// In Day20
private fun MutableList<MappedNumber>.decrypt() {
indices.forEach { originalIndex ->
val index = indexOfFirst { it.originalIndex == originalIndex }
val toBeMoved = removeAt(index)
add((index + toBeMoved.value).mod(size), toBeMoved)
}
}
Since we need to go through our input
in order, and account for duplicates, we need to find the MappedNumber
in our list via its originalIndex
. The way I’ve chosen to do this is to get the indicies
off the current list, which will be the same size (we could have also explicitly set up a range, which is what I did at first before I remembered indices
). Once we find the index
of the MappedNumber
we’re after, we remove it from the list, and set it aside in toBeMoved
. Then we add
it back in, accounting for the fact that we need to use mod
in order for the circular list thing to work out.
And now can put all this together in a single solvePart1
function to get our answer:
// In Day20
fun solvePart1(): Long {
val theList = parseInput()
theList.decrypt()
return theList.groveCoordinates()
}
Star earned! Onward!
⭐ Day 20, Part 2
The puzzle text can be found here.
In order to account for the decryptionKey
we need to do a bit of refactoring to our parseInput
function. Since Kotlin has default values, we can set the default to 1 so we don’t have to alter solvePart1
. In our case, that wouldn’t be a big deal. But if you maintain a larger system with more than one user of a service or something, adding a defaulted value is a nice way to not break your callers.
The only change to the implementation is to multiply the value
by the decriptionKey
before storing it in the MappedNumber
.
// In Day20
private fun parseInput(decryptionKey: Long = 1L): MutableList<MappedNumber> =
input.mapIndexed { index, value ->
MappedNumber(index, decryptionKey * value.toLong())
}.toMutableList()
We’re now able to write our solvePart2
function. We call parseInput
with the decryptionKey
, and get a list similar to what we had in Part One. We’ll repeat
the decrypt
process 10 times, and then get the groveCoordinates
to calculate our answer.
// In Day20
fun solvePart2(): Long {
val theList = parseInput(811589153)
repeat(10) {
theList.decrypt()
}
return theList.groveCoordinates()
}
Star earned! See you tomorrow.
Further Reading
- Index of All Solutions - All posts and solutions for 2022, in Kotlin.
- My Github repo - Solutions and tests for each day.
- Solution - Full code for day 20
- Advent of Code - Come join in and do these challenges yourself!