Advent of Code in Kotlin 2021 - Day 22
This is part 22 of a series, so if you haven't read the previous parts, start here.
I really enjoyed today's puzzle and am happy with my solution for the first time in days.
The key to an efficient implementation is tracking cuboids rather than individual cubes. I made heavy use of Kotlin operator overloads to make it straightforward to subtract one cuboid from another resulting in a new set of cuboids.
For every instruction I subtracted the relevant cuboid from all existing ones before adding it to the set if the instruction was "on". Writing that operator function was a bit of work and required a fair bit of spatial imagination, but once that was in place, the rest was easy.
By turning the initialization procedure region off and seeing how many
fun main() {
val instructions = inputLines(2021, 22).map(::parseInstruction)
val cuboidsThatAreOn = instructions.fold(emptySet<Cuboid>()) { set, instruction -> set.apply(instruction) }
val cubesThatAreOn = cuboidsThatAreOn.sumOf { it.size }
val initializationProcedureRegion = Cuboid(-50 .. 50, -50 .. 50, -50 .. 50)
println(
cubesThatAreOn - cuboidsThatAreOn
.apply(Instruction(InstructionType.OFF, initializationProcedureRegion))
.sumOf { it.size })
println(cubesThatAreOn)
}
private fun parseInstruction(line: String): Instruction {
val type = if (line.startsWith("on")) InstructionType.ON else InstructionType.OFF
val ranges = line.substringAfter(" ").split(",")
return Instruction(type, Cuboid(parseRange(ranges[0]), parseRange(ranges[1]), parseRange(ranges[2])))
}
private fun parseRange(string: String): IntRange = string
.substring(2, string.length)
.split("..").map(String::toInt)
.let { (from, to) -> (from..to) }
private data class Instruction(val type: InstructionType, val cuboid: Cuboid)
private enum class InstructionType { ON, OFF }
private data class Cuboid(val x: IntRange, val y: IntRange, val z: IntRange) {
val size: Long
get() = x.size.toLong() * y.size * z.size
fun overlaps(other: Cuboid): Boolean = x.overlaps(other.x) && y.overlaps(other.y) && z.overlaps(other.z)
operator fun minus(other: Cuboid): Set<Cuboid> {
return setOfNotNull(
if (other.x.first > x.first) {
Cuboid(x.first until other.x.first, y, z)
} else null,
if (other.y.first > y.first) {
Cuboid(x.overlapWith(other.x), y.first until other.y.first, z)
} else null,
if (other.y.last < y.last) {
Cuboid(x.overlapWith(other.x), other.y.last + 1 .. y.last, z)
} else null,
if (other.z.first > z.first) {
Cuboid(x.overlapWith(other.x), y.overlapWith(other.y), z.first until other.z.first)
} else null,
if (other.z.last < z.last) {
Cuboid(x.overlapWith(other.x), y.overlapWith(other.y), other.z.last + 1 .. z.last)
} else null,
if (other.x.last < x.last) {
Cuboid(other.x.last + 1 .. x.last, y, z)
} else null,
)
}
}
private fun Set<Cuboid>.apply(instruction: Instruction): Set<Cuboid> {
val (type, cuboid) = instruction
return this.flatMap { if (it.overlaps(cuboid)) it - cuboid else setOf(it) }.toSet().let {
if (type == InstructionType.ON) it.plus(cuboid) else it
}
}
private fun IntRange.overlaps(other: IntRange): Boolean = first <= other.last && last >= other.first
private fun IntRange.overlapWith(other: IntRange): IntRange = max(first, other.first) .. min(last, other.last)
private val IntRange.size: Int
get() = last - first + 1
Two more stars in the bag.