generated from Jadarma/advent-of-code-kotlin-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathY2021D23.kt
157 lines (136 loc) · 6.51 KB
/
Y2021D23.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package aockt.y2021
import aockt.util.parse
import aockt.util.Pathfinding
import io.github.jadarma.aockt.core.Solution
import kotlin.math.abs
object Y2021D23 : Solution {
/**
* The types of amphipods.
* @property preferredPosition The coordinate of the destination room.
* @property energyPerStep How much cost does every step incur.
*/
private enum class AmphipodType(val preferredPosition: Int, val energyPerStep: Int) {
Amber(2, 1),
Bronze(4, 10),
Copper(6, 100),
Desert(8, 1000),
}
/**
* The state of a single amphipod.
* @property type The type of amphipod.
* @property position The horizontal position in the area.
* @property depth The vertical position in the room, or zero if in the hallway.
*/
private data class Amphipod(val type: AmphipodType, val position: Int, val depth: Int)
/**
* A search state.
* @property pods The positions of all amphipods.
* @property roomDepth How many amphipods can fit in a room.
*/
private data class State(val pods: Set<Amphipod>, val roomDepth: Int = 2) :
Set<Amphipod> by pods {
fun neighbors(): Set<Pair<State, Int>> = buildSet {
// Get pods that are still not in their final position:
// - Any pod in the hallway.
// - Any pod in the wrong room.
// - Any pod in a room that obstructs another pod that is in the wrong room.
val needToMove = pods.filter {
it.depth == 0
|| it.position != it.type.preferredPosition
|| pods.any { other ->
other.position == it.position && other.depth > it.depth && other.type.preferredPosition != it.position
}
}
val (inHallway, inRooms) = needToMove.partition { it.depth == 0 }
// A pod that needs to move out of the hallway can only do so if its preferred room is empty or only
// contains similar pods, and there is no other pod occupying the hallway path to said room.
val canMoveInRooms = inHallway
.filter {
pods
.filter { other -> other.position == it.type.preferredPosition }
.none { other -> other.type != it.type }
}
.filter {
val hallwaySegment =
if (it.position < it.type.preferredPosition) it.position + 1..it.type.preferredPosition
else it.type.preferredPosition..<it.position
inHallway.none { other -> other.position in hallwaySegment }
}
canMoveInRooms.forEach {
val destinationDepth = pods
.filter { other -> other.position == it.type.preferredPosition }
.minOfOrNull { other -> other.depth }
?.dec()
?: roomDepth
val newPod = it.copy(position = it.type.preferredPosition, depth = destinationDepth)
val moveCost = (abs(it.type.preferredPosition - it.position) + destinationDepth) * it.type.energyPerStep
val newState = copy(pods = pods.minus(it).plus(newPod))
add(newState to moveCost)
}
// A pod that needs to move out of the room can do so if it is the first one and there is hallway space.
val takenHallwaySlots = inHallway.map { it.position }.toSet()
val canMoveOutOfRooms = inRooms
.filter {
it.depth == 1 || pods.none { other -> other.position == it.position && other.depth < it.depth }
}
.map {
val leftHalf = (it.position - 1 downTo 0).takeWhile { slot -> slot !in takenHallwaySlots }
val rightHalf = (it.position + 1..10).takeWhile { slot -> slot !in takenHallwaySlots }
it to (leftHalf + rightHalf).filter { slot -> slot in hallwaySlots }
}
canMoveOutOfRooms.forEach { (pod, hallWaySlot) ->
for (slot in hallWaySlot) {
val newPod = pod.copy(position = slot, depth = 0)
val moveCost = (abs(slot - pod.position) + pod.depth) * pod.type.energyPerStep
val newState = copy(pods = pods.minus(pod).plus(newPod))
add(newState to moveCost)
}
}
}
/** The state is final if none of the pods stand in the hallway and all of them are in their preferred room. */
fun isFinal(): Boolean = pods.all { it.depth > 0 && it.position == it.type.preferredPosition }
private companion object {
/** Resting places in the hallway. */
val hallwaySlots = setOf(0, 1, 3, 5, 7, 9, 10)
}
}
/** Parse the [input] and return the starting [State], optionally making the rooms [deep]. */
private fun parseInput(input: String, deep: Boolean): State = parse {
val regex = Regex("""^#{13}\n#\.{11}#\n#{3}(?:[A-D]#){4}##\n {2}#(?:[A-D]#){4}\n {2}#{9}$""")
require(input.matches(regex)) { "Invalid input." }
val pods = input
.filter { it in "ABCD" }
.run { if (deep) "${substring(0, 4)}DCBADBAC${substring(4, 8)}" else this }
.chunked(4)
.withIndex()
.flatMap { (depth, chunk) ->
chunk.mapIndexed { position, c ->
Amphipod(
position = position * 2 + 2,
depth = depth + 1,
type = when (c) {
'A' -> AmphipodType.Amber
'B' -> AmphipodType.Bronze
'C' -> AmphipodType.Copper
'D' -> AmphipodType.Desert
else -> error("Unreachable.")
},
)
}
}
.toSet()
State(pods = pods, roomDepth = if (deep) 4 else 2)
}
/** Common solution for both parts. */
private fun solve(input: String, deep: Boolean): Int =
Pathfinding
.search(
start = parseInput(input, deep),
neighbours = { it.neighbors() },
goalFunction = { it.isFinal() },
)
?.cost
?: -1
override fun partOne(input: String): Int = solve(input, deep = false)
override fun partTwo(input: String): Int = solve(input, deep = true)
}