day(23): add solution
Signed-off-by: Matej Focko <mfocko@redhat.com>
This commit is contained in:
parent
7a45c5db03
commit
13b2f0fb92
3 changed files with 232 additions and 0 deletions
220
src/year2021/day23/Day23.kt
Normal file
220
src/year2021/day23/Day23.kt
Normal file
|
@ -0,0 +1,220 @@
|
|||
package year2021.day23
|
||||
|
||||
import product
|
||||
import readInput
|
||||
import java.util.PriorityQueue
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
// region Position
|
||||
data class Position(val x: Int, val y: Int) {
|
||||
fun distance(other: Position): Int =
|
||||
if (y != 1 && other.y != 1) {
|
||||
val mid = Position(other.x, 1)
|
||||
this.distance(mid) + mid.distance(other)
|
||||
} else {
|
||||
(x - other.x).absoluteValue + (y - other.y).absoluteValue
|
||||
}
|
||||
}
|
||||
// endregion Position
|
||||
|
||||
// region Ampod
|
||||
fun energyPerStep(type: Char): Int = when (type) {
|
||||
'A' -> 1
|
||||
'B' -> 10
|
||||
'C' -> 100
|
||||
'D' -> 1000
|
||||
else -> error("Invalid amphipod type")
|
||||
}
|
||||
|
||||
fun targetRoom(type: Char): Int = when (type) {
|
||||
'A' -> 3
|
||||
'B' -> 5
|
||||
'C' -> 7
|
||||
'D' -> 9
|
||||
else -> error("Invalid amphipod type")
|
||||
}
|
||||
|
||||
data class Ampod(val type: Char, val position: Position) {
|
||||
fun costTo(position: Position): Int = this.position.distance(position) * energyPerStep(type)
|
||||
|
||||
val isInHallway: Boolean
|
||||
get() = position.y == 1
|
||||
|
||||
val isInRoom: Boolean
|
||||
get() = position.y in 2..5 && position.x in (3..9 step 2)
|
||||
|
||||
val isInCorrectRoom: Boolean
|
||||
get() = position.y in 2..5 && position.x == targetRoom(type)
|
||||
}
|
||||
// endregion Ampod
|
||||
|
||||
// region Diagram
|
||||
data class Diagram(val ampods: Set<Ampod>, val maxY: Int = 3) {
|
||||
private val hallwaySpots =
|
||||
(1 until 12)
|
||||
.filter { (it < 3 || it > 9 || it in (4..8 step 2)) }
|
||||
.map { x -> Position(x, 1) }
|
||||
|
||||
private val freeHallwaySpots: Sequence<Position>
|
||||
get() = hallwaySpots.asSequence().filter { !ampods.any { amphipod -> amphipod.position == it } }
|
||||
|
||||
private val ampodsInHallway: Sequence<Ampod>
|
||||
get() = ampods.asSequence().filter(Ampod::isInHallway)
|
||||
|
||||
private val ampodsInRooms: Sequence<Ampod>
|
||||
get() = ampods.asSequence().filter(Ampod::isInRoom)
|
||||
|
||||
// TODO: Refactor
|
||||
private fun blocked(src: Position, dst: Position): Boolean {
|
||||
if (src.y == 1 && dst.y in 2..maxY) {
|
||||
return ampodsInHallway.any {
|
||||
(it.position.x > src.x && it.position.x <= dst.x) || (it.position.x < src.x && it.position.x >= dst.x)
|
||||
} || ampods.any {
|
||||
it.position.x == dst.x && it.position.y <= dst.y
|
||||
}
|
||||
} else if (src.y in 2..maxY && dst.y == 1) {
|
||||
return ampods.any {
|
||||
it.position.x == src.x && it.position.y < src.y
|
||||
} || ampodsInHallway.any {
|
||||
(it.position.x in src.x..dst.x) || (it.position.x in dst.x..src.x)
|
||||
}
|
||||
}
|
||||
|
||||
return blocked(src, Position(dst.x, 1)) || blocked(Position(dst.x, 1), dst)
|
||||
}
|
||||
|
||||
private fun moveAmpod(ampod: Ampod, position: Position): Diagram =
|
||||
this.copy(ampods = ampods - setOf(ampod) + setOf(ampod.copy(position = position)))
|
||||
|
||||
private fun tryMoveFromHallway(ampod: Ampod): Pair<Diagram, Int>? {
|
||||
val targetX = targetRoom(ampod.type)
|
||||
|
||||
if (ampodsInRooms.any { it.type != ampod.type && it.position.x == targetX }) {
|
||||
// there is still an ampod of different type in the room
|
||||
return null
|
||||
}
|
||||
|
||||
val destination =
|
||||
Position(
|
||||
targetX,
|
||||
(2..maxY).reversed().first { y ->
|
||||
!ampodsInRooms.any {
|
||||
it.type == ampod.type && it.position == Position(
|
||||
targetX, y
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (blocked(ampod.position, destination)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (moveAmpod(ampod, destination) to ampod.costTo(destination))
|
||||
}
|
||||
|
||||
private fun tryMoveFromRoom(ampod: Ampod, destination: Position): Pair<Diagram, Int>? {
|
||||
if (blocked(ampod.position, destination)) {
|
||||
return null
|
||||
}
|
||||
return moveAmpod(ampod, destination) to ampod.costTo(destination)
|
||||
}
|
||||
|
||||
fun moves(): Sequence<Pair<Diagram, Int>> = sequence {
|
||||
yieldAll(product(ampodsInRooms, freeHallwaySpots).mapNotNull { (amphipod, freeSpot) ->
|
||||
tryMoveFromRoom(amphipod, freeSpot)
|
||||
})
|
||||
yieldAll(ampodsInHallway.mapNotNull { tryMoveFromHallway(it) })
|
||||
}
|
||||
|
||||
val goal: Diagram
|
||||
get() = this.copy(
|
||||
ampods = product('A'..'D', 2..maxY).map { (type, y) ->
|
||||
Ampod(
|
||||
type,
|
||||
Position(targetRoom(type), y)
|
||||
)
|
||||
}.toSet()
|
||||
)
|
||||
|
||||
override fun toString(): String {
|
||||
fun charAt(x: Int, y: Int): Char =
|
||||
if (y > 2 && (x < 2 || x > 10))
|
||||
' '
|
||||
else if (y == 0 || y == 4 || x == 0 || x == 12)
|
||||
'#'
|
||||
else if (y == 1 || x == 3 || x == 5 || x == 7 || x == 9)
|
||||
ampods.firstOrNull { it.position == Position(x, y) }?.type ?: '.'
|
||||
else
|
||||
'#'
|
||||
|
||||
return (0 until 5).joinToString("\n") { y ->
|
||||
(0 until 13).map { x -> charAt(x, y) }.joinToString("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun List<String>.toDiagram(): Diagram = Diagram(
|
||||
this@toDiagram.flatMapIndexed { y, row ->
|
||||
row.mapIndexedNotNull { x, c ->
|
||||
if (c !in 'A'..'D') {
|
||||
null
|
||||
} else {
|
||||
Ampod(c, Position(x, y))
|
||||
}
|
||||
}
|
||||
}.toSet()
|
||||
)
|
||||
// endregion Diagram
|
||||
|
||||
// region solve
|
||||
fun solve(state: Diagram, goal: Diagram): Int {
|
||||
val cost = mutableMapOf(state to 0)
|
||||
val visited = mutableSetOf<Diagram>()
|
||||
val queue = PriorityQueue<Pair<Diagram, Int>> { a, b ->
|
||||
a.second - b.second
|
||||
}
|
||||
|
||||
queue.add(state to 0)
|
||||
|
||||
while (queue.isNotEmpty()) {
|
||||
val (currentDiagram, currentCost) = queue.remove()
|
||||
if (visited.contains(currentDiagram)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (currentDiagram == goal) {
|
||||
break
|
||||
}
|
||||
|
||||
visited.add(currentDiagram)
|
||||
currentDiagram.moves().forEach { (next, nextCost) ->
|
||||
val alternativeCost = currentCost + nextCost
|
||||
if (alternativeCost < cost.getOrDefault(next, Int.MAX_VALUE)) {
|
||||
cost[next] = alternativeCost
|
||||
queue.add(next to alternativeCost)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cost[goal]!!
|
||||
}
|
||||
// endregion solve
|
||||
|
||||
fun part1(input: Diagram): Int = solve(input, input.goal)
|
||||
fun part2(input: Diagram): Int = input.copy(maxY = 5).let { newInput -> solve(newInput, newInput.goal) }
|
||||
|
||||
fun main() {
|
||||
val sample = readInput(23, "sample").toDiagram()
|
||||
val input = readInput(23, "input").toDiagram()
|
||||
|
||||
check(part1(sample) == 12521)
|
||||
println("[PASS] Part #1 check")
|
||||
println(part1(input))
|
||||
|
||||
val sample2 = readInput(23, "sample2").toDiagram()
|
||||
val input2 = readInput(23, "input2").toDiagram()
|
||||
|
||||
check(part2(sample2) == 44169)
|
||||
println("[PASS] Part #2 check")
|
||||
println(part2(input2))
|
||||
}
|
5
src/year2021/day23/sample.txt
Normal file
5
src/year2021/day23/sample.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
#############
|
||||
#...........#
|
||||
###B#C#B#D###
|
||||
#A#D#C#A#
|
||||
#########
|
7
src/year2021/day23/sample2.txt
Normal file
7
src/year2021/day23/sample2.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
#############
|
||||
#...........#
|
||||
###B#C#B#D###
|
||||
#D#C#B#A#
|
||||
#D#B#A#C#
|
||||
#A#D#C#A#
|
||||
#########
|
Loading…
Reference in a new issue